diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js index d83155d..f81e6ca 100644 --- a/backend/controllers/userController.js +++ b/backend/controllers/userController.js @@ -194,7 +194,8 @@ const uploadUserProfilePhoto = async (req, res, next) => { const newPhoto = { url: result.secure_url, public_id: result.public_id, - isProfilePhoto: user.photos.length === 0, + // Если это первая фотография, делаем ее главной. Иначе - нет. + isProfilePhoto: user.photos.length === 0, }; // Добавляем новое фото в массив фотографий пользователя @@ -202,19 +203,23 @@ const uploadUserProfilePhoto = async (req, res, next) => { await user.save(); // Получаем обновленного пользователя с обновленным массивом фотографий - const updatedUser = await User.findById(req.user._id); + const updatedUser = await User.findById(req.user._id).lean(); // .lean() для простого объекта // Важно! Явно создаем массив allPhotos для ответа, чтобы убедиться, что структура каждого объекта фото совпадает const allPhotos = updatedUser.photos.map(photo => ({ + _id: photo._id, // Добавляем _id для фото url: photo.url, public_id: photo.public_id, isProfilePhoto: photo.isProfilePhoto })); + // Найдем только что добавленное фото в обновленном массиве, чтобы вернуть его с _id + const addedPhotoWithId = allPhotos.find(p => p.public_id === newPhoto.public_id); + // Возвращаем ответ с точной структурой, ожидаемой тестами res.status(200).json({ message: 'Фотография успешно загружена!', - photo: newPhoto, + photo: addedPhotoWithId, // Возвращаем фото с _id allPhotos: allPhotos }); @@ -227,12 +232,115 @@ const uploadUserProfilePhoto = async (req, res, next) => { } }; -// Другие возможные функции для userController (например, getUserById, getAllUsers для админа и т.д.) -// const getUserById = async (req, res, next) => { ... }; +// @desc Установить фотографию как главную +// @route PUT /api/users/profile/photo/:photoId/set-main +// @access Private +const setMainPhoto = async (req, res, next) => { + try { + const user = await User.findById(req.user._id); + const { photoId } = req.params; + + if (!user) { + const error = new Error('Пользователь не найден.'); + error.statusCode = 404; + return next(error); + } + + let photoFound = false; + user.photos.forEach(photo => { + if (photo._id.toString() === photoId) { + photo.isProfilePhoto = true; + photoFound = true; + } else { + photo.isProfilePhoto = false; + } + }); + + if (!photoFound) { + const error = new Error('Фотография не найдена.'); + error.statusCode = 404; + return next(error); + } + + await user.save(); + const updatedUser = await User.findById(req.user._id).lean(); + + res.status(200).json({ + message: 'Главная фотография успешно обновлена.', + photos: updatedUser.photos.map(p => ({_id: p._id, url: p.url, public_id: p.public_id, isProfilePhoto: p.isProfilePhoto })) + }); + + } catch (error) { + console.error('[USER_CTRL] Ошибка при установке главной фотографии:', error.message); + next(error); + } +}; + +// @desc Удалить фотографию пользователя +// @route DELETE /api/users/profile/photo/:photoId +// @access Private +const deletePhoto = async (req, res, next) => { + try { + const user = await User.findById(req.user._id); + const { photoId } = req.params; + + if (!user) { + const error = new Error('Пользователь не найден.'); + error.statusCode = 404; + return next(error); + } + + const photoIndex = user.photos.findIndex(p => p._id.toString() === photoId); + + if (photoIndex === -1) { + const error = new Error('Фотография не найдена.'); + error.statusCode = 404; + return next(error); + } + + const photoToDelete = user.photos[photoIndex]; + + // Удаление из Cloudinary + if (photoToDelete.public_id) { + try { + await cloudinary.uploader.destroy(photoToDelete.public_id); + console.log(`[USER_CTRL] Фото ${photoToDelete.public_id} удалено из Cloudinary.`); + } catch (cloudinaryError) { + console.error('[USER_CTRL] Ошибка при удалении фото из Cloudinary:', cloudinaryError.message); + // Не прерываем процесс, если фото не удалось удалить из Cloudinary, но логируем ошибку + // Можно добавить более сложную логику обработки, например, пометить фото для последующего удаления + } + } + + // Удаление из массива photos пользователя + user.photos.splice(photoIndex, 1); + + // Если удаленная фотография была главной и остались другие фотографии, + // делаем первую из оставшихся главной. + if (photoToDelete.isProfilePhoto && user.photos.length > 0) { + user.photos[0].isProfilePhoto = true; + } + + await user.save(); + const updatedUser = await User.findById(req.user._id).lean(); + + res.status(200).json({ + message: 'Фотография успешно удалена.', + photos: updatedUser.photos.map(p => ({_id: p._id, url: p.url, public_id: p.public_id, isProfilePhoto: p.isProfilePhoto })) + }); + + } catch (error) { + console.error('[USER_CTRL] Ошибка при удалении фотографии:', error.message); + next(error); + } +}; + module.exports = { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto, + setMainPhoto, // <--- Добавлено + deletePhoto, // <--- Добавлено // getUserById, }; \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index f5363f8..a74052e 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto } = require('../controllers/userController'); +const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto, setMainPhoto, deletePhoto } = require('../controllers/userController'); const { protect } = require('../middleware/authMiddleware'); // Нам нужен protect для защиты маршрута const multer = require('multer'); // 1. Импортируем multer const path = require('path'); // Может понадобиться для фильтрации файлов @@ -39,6 +39,8 @@ router.get('/suggestions', protect, getUsersForSwiping); // <--- НОВЫЙ МА // Маршрут для загрузки фотографии профиля // POST /api/users/profile/photo router.post('/profile/photo', protect, upload.single('profilePhoto'), uploadUserProfilePhoto); +router.put('/profile/photo/:photoId/set-main', protect, setMainPhoto); // <--- НОВЫЙ МАРШРУТ +router.delete('/profile/photo/:photoId', protect, deletePhoto); // <--- НОВЫЙ МАРШРУТ // Маршрут для получения профиля по ID (например, для просмотра чужих профилей, если это нужно) // GET /api/users/:id diff --git a/package-lock.json b/package-lock.json index d886d2f..8596527 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "axios": "^1.9.0", "bcryptjs": "^3.0.2", "bootstrap": "^5.3.6", + "browser-image-compression": "^2.0.2", "cloudinary": "^2.6.1", + "compressorjs": "^1.2.1", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", @@ -2816,6 +2818,12 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -2866,6 +2874,15 @@ "concat-map": "0.0.1" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -3088,6 +3105,16 @@ "node": ">=4.0.0" } }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "license": "MIT", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4343,6 +4370,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -6683,6 +6722,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index bafa834..24b0afb 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "axios": "^1.9.0", "bcryptjs": "^3.0.2", "bootstrap": "^5.3.6", + "browser-image-compression": "^2.0.2", "cloudinary": "^2.6.1", + "compressorjs": "^1.2.1", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/src/components/EditProfileForm.vue b/src/components/EditProfileForm.vue index aa9a6d0..f6a30a3 100644 --- a/src/components/EditProfileForm.vue +++ b/src/components/EditProfileForm.vue @@ -37,10 +37,10 @@

Загрузить фото профиля

-
+
- - + +
@@ -51,7 +51,11 @@
{{ photoUploadSuccessMessage }}
{{ photoUploadErrorMessage }}
- @@ -63,6 +67,7 @@ import { ref, onMounted, watch } from 'vue'; import { useAuth } from '@/auth'; import api from '@/services/api'; + import imageCompression from 'browser-image-compression'; // Импортируем библиотеку const { user, fetchUser } = useAuth(); @@ -80,9 +85,13 @@ const photoLoading = ref(false); const photoUploadErrorMessage = ref(''); const photoUploadSuccessMessage = ref(''); - const selectedFile = ref(null); - const previewUrl = ref(null); + const selectedFile = ref(null); // Это будет использоваться для одиночной загрузки, если мы решим ее оставить + const previewUrl = ref(null); // И это + // Новые refs для множественной загрузки + const selectedFiles = ref([]); // Массив выбранных файлов + const photoUploadMessages = ref([]); // Массив сообщений о статусе загрузки каждого файла + const initializeFormData = (currentUser) => { console.log('[EditProfileForm] Инициализация формы с данными:', currentUser); if (currentUser) { @@ -90,6 +99,7 @@ formData.value.bio = currentUser.bio || ''; formData.value.dateOfBirth = currentUser.dateOfBirth ? new Date(currentUser.dateOfBirth).toISOString().split('T')[0] : ''; formData.value.gender = currentUser.gender || ''; + // Не очищаем photoUploadMessages здесь, чтобы пользователь видел результаты предыдущих загрузок в сессии } }; @@ -137,61 +147,101 @@ } }; - const onFileSelected = (event) => { - const file = event.target.files[0]; - if (file) { - selectedFile.value = file; - // Создание URL для предпросмотра - const reader = new FileReader(); - reader.onload = (e) => { - previewUrl.value = e.target.result; - }; - reader.readAsDataURL(file); - photoUploadErrorMessage.value = ''; // Сброс ошибки при выборе нового файла - photoUploadSuccessMessage.value = ''; // Сброс сообщения об успехе - } else { + // Вместо 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: 'Ожидает загрузки...' + })); + // Очищаем старые одиночные состояния, если они были selectedFile.value = null; previewUrl.value = null; + photoUploadErrorMessage.value = ''; + photoUploadSuccessMessage.value = ''; + } else { + selectedFiles.value = []; + photoUploadMessages.value = []; } }; - const handlePhotoUpload = async () => { - if (!selectedFile.value) { - photoUploadErrorMessage.value = 'Пожалуйста, выберите файл для загрузки.'; + // Обновляем handlePhotoUpload для загрузки нескольких файлов последовательно + const handleMultiplePhotoUpload = async () => { + if (selectedFiles.value.length === 0) { + photoUploadErrorMessage.value = 'Пожалуйста, выберите файлы для загрузки.'; + // Очистим индивидуальные сообщения, если они были + photoUploadMessages.value.forEach(msg => { + if (msg.status === 'pending') msg.status = 'cancelled'; + }); return; } - console.log('[EditProfileForm] Загрузка фото...'); - photoLoading.value = true; - photoUploadErrorMessage.value = ''; - photoUploadSuccessMessage.value = ''; - const fd = new FormData(); - fd.append('profilePhoto', selectedFile.value, selectedFile.value.name); + photoLoading.value = true; // Общий индикатор загрузки + photoUploadErrorMessage.value = ''; // Сбрасываем общую ошибку - try { - console.log('[EditProfileForm] Отправка фото на сервер:', selectedFile.value.name); - const response = await api.uploadUserProfilePhoto(fd); // Предполагается, что такой метод есть в api.js - console.log('[EditProfileForm] Ответ от сервера (фото):', response.data); + 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; - photoUploadSuccessMessage.value = response.data.message || 'Фото успешно загружено!'; - selectedFile.value = null; - previewUrl.value = null; - // Очищаем поле выбора файла визуально (браузер может не сбросить его сам) - const photoInput = document.getElementById('profilePhoto'); - if (photoInput) { - photoInput.value = ''; + photoUploadMessages.value[fileMessageIndex].status = 'uploading'; + photoUploadMessages.value[fileMessageIndex].message = 'Сжатие и загрузка...'; + + 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}.`; + } } - await fetchUser(); // Обновляем данные пользователя, включая массив photos - console.log('[EditProfileForm] Данные пользователя обновлены (фото)'); - - } catch (err) { - console.error('[EditProfileForm] Ошибка при загрузке фото:', err); - photoUploadErrorMessage.value = (err.response && err.response.data && err.response.data.message) - ? err.response.data.message - : 'Ошибка при загрузке фото.'; - } finally { - photoLoading.value = false; } + + // После завершения всех загрузок + selectedFiles.value = []; // Очищаем выбранные файлы + // Не очищаем photoInput.value здесь, т.к. input type="file" multiple сам управляет списком + // Можно сбросить значение инпута, чтобы пользователь мог выбрать те же файлы снова, если захочет + const photoInput = document.getElementById('profilePhotos'); // Убедимся, что ID правильный + if (photoInput) { + photoInput.value = ''; + } + photoLoading.value = false; // Выключаем общий индикатор }; @@ -208,4 +258,16 @@ background-color: #fff; border-radius: 0.25rem; } + .upload-message { + font-size: 0.9em; + } + .upload-message.success { + color: green; + } + .upload-message.error { + color: red; + } + .upload-message.pending, .upload-message.uploading { + color: orange; + } \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index 64eed54..4cd4390 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -88,6 +88,13 @@ export default { }, }); }, + // Новые методы для управления фотографиями + setMainPhoto(photoId) { + return apiClient.put(`/users/profile/photo/${photoId}/set-main`); + }, + deletePhoto(photoId) { + return apiClient.delete(`/users/profile/photo/${photoId}`); + }, getUserConversations() { return apiClient.get('/conversations'); // GET /api/conversations }, diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue index e7cf2e5..9a5d9bc 100644 --- a/src/views/ProfileView.vue +++ b/src/views/ProfileView.vue @@ -25,9 +25,30 @@
Мои фотографии
+

{{ photoActionError }}

+

{{ photoActionSuccess }}

-
- Фото пользователя +
+
+ Фото пользователя +
+ + +
+ Главная +
@@ -44,6 +65,28 @@

Не удалось загрузить данные профиля. Возможно, вы не авторизованы или произошла ошибка.

Войти
+ + +
@@ -51,15 +94,24 @@ import { ref, onMounted, computed, watch } from 'vue'; // Добавлен watch import { useAuth } from '@/auth'; import api from '@/services/api'; -import router from '@/router'; +// import router from '@/router'; // Закомментировано, так как router не используется напрямую import EditProfileForm from '@/components/EditProfileForm.vue'; +import { Modal } from 'bootstrap'; // Импортируем Modal -const { isAuthenticated, user: authUserFromStore, token, fetchUser } = useAuth(); // Добавлен fetchUser +const { isAuthenticated, user: authUserFromStore, token, fetchUser } = useAuth(); const profileData = ref(null); const loading = ref(true); const initialLoading = ref(true); const error = ref(''); +// Для управления фото +const photoActionLoading = ref(false); +const photoActionError = ref(''); +const photoActionSuccess = ref(''); +const photoToDeleteId = ref(null); +let deleteModalInstance = null; + + const fetchProfileDataLocal = async () => { loading.value = true; error.value = ''; @@ -72,10 +124,7 @@ const fetchProfileDataLocal = async () => { } try { - // Вместо прямого вызова api.getMe(), используем fetchUser из useAuth, - // который обновит authUserFromStore, за которым мы будем следить. await fetchUser(); - // profileData.value будет обновляться через watch } catch (err) { console.error('[ProfileView] Ошибка при вызове fetchUser:', err); error.value = (err.response && err.response.data && err.response.data.message) @@ -87,27 +136,26 @@ const fetchProfileDataLocal = async () => { } }; -// Следим за изменениями authUserFromStore и обновляем profileData watch(authUserFromStore, (newUser) => { if (newUser) { - profileData.value = { ...newUser }; // Клонируем, чтобы избежать прямой мутации, если это необходимо + profileData.value = { ...newUser }; console.log('[ProfileView] Данные профиля обновлены из authUserFromStore:', profileData.value); } -}, { immediate: true, deep: true }); // immediate: true для инициализации при монтировании +}, { immediate: true, deep: true }); -onMounted(() => { - // Если authUserFromStore уже содержит данные (например, после логина), - // они будут установлены через watch immediate: true. - // Если нет, или мы хотим принудительно обновить, вызываем fetchProfileDataLocal. +onMounted(async () => { if (!authUserFromStore.value || Object.keys(authUserFromStore.value).length === 0) { - fetchProfileDataLocal(); + await fetchProfileDataLocal(); } else { - // Данные уже есть, initialLoading можно установить в false - // loading также, так как данные уже есть - profileData.value = { ...authUserFromStore.value }; - loading.value = false; - initialLoading.value = false; - console.log('[ProfileView] Данные профиля уже были в хранилище:', profileData.value); + profileData.value = { ...authUserFromStore.value }; + loading.value = false; + initialLoading.value = false; + console.log('[ProfileView] Данные профиля уже были в хранилище:', profileData.value); + } + // Инициализация модального окна + const modalElement = document.getElementById('deletePhotoModal'); + if (modalElement) { + deleteModalInstance = new Modal(modalElement); } }); @@ -116,6 +164,51 @@ const formatDate = (dateString) => { const options = { year: 'numeric', month: 'long', day: 'numeric' }; return new Date(dateString).toLocaleDateString(undefined, options); }; + +const setAsMainPhoto = async (photoId) => { + photoActionLoading.value = true; + photoActionError.value = ''; + photoActionSuccess.value = ''; + try { + const response = await api.setMainPhoto(photoId); + await fetchUser(); // Обновляем данные пользователя (и profileData через watch) + photoActionSuccess.value = response.data.message || 'Главное фото обновлено.'; + } catch (err) { + console.error('[ProfileView] Ошибка при установке главного фото:', err); + photoActionError.value = err.response?.data?.message || 'Не удалось установить главное фото.'; + } finally { + photoActionLoading.value = false; + } +}; + +const confirmDeletePhoto = (photoId) => { + photoToDeleteId.value = photoId; + if (deleteModalInstance) { + deleteModalInstance.show(); + } +}; + +const executeDeletePhoto = async () => { + if (!photoToDeleteId.value) return; + photoActionLoading.value = true; + photoActionError.value = ''; + photoActionSuccess.value = ''; + try { + const response = await api.deletePhoto(photoToDeleteId.value); + await fetchUser(); // Обновляем данные пользователя + photoActionSuccess.value = response.data.message || 'Фотография удалена.'; + } catch (err) { + console.error('[ProfileView] Ошибка при удалении фото:', err); + photoActionError.value = err.response?.data?.message || 'Не удалось удалить фотографию.'; + } finally { + photoActionLoading.value = false; + if (deleteModalInstance) { + deleteModalInstance.hide(); + } + photoToDeleteId.value = null; + } +}; + \ No newline at end of file diff --git a/src/views/SwipeView.vue b/src/views/SwipeView.vue index b7672b7..d2a792b 100644 --- a/src/views/SwipeView.vue +++ b/src/views/SwipeView.vue @@ -21,19 +21,44 @@
- -
- - -
- + + + +
+
@@ -197,15 +222,25 @@ const formatGender = (gender) => { return ''; // Возвращаем пустую строку, если пол не указан, чтобы не было "Пол не указан" }; -const onImageError = (event, userOnError) => { - console.warn("Не удалось загрузить изображение:", event.target.src, "для пользователя:", userOnError.name); - // Найдем пользователя в suggestions и установим mainPhotoUrl в null - // чтобы v-else сработал корректно и реактивно +const onImageError = (event, userOnError, photoIdOnError) => { + console.warn("Не удалось загрузить изображение:", event.target.src, "для пользователя:", userOnError.name, "фото ID:", photoIdOnError); const userInSuggestions = suggestions.value.find(u => u._id === userOnError._id); - if (userInSuggestions) { - userInSuggestions.mainPhotoUrl = null; + if (userInSuggestions && userInSuggestions.photos) { + const photoIndex = userInSuggestions.photos.findIndex(p => p._id === photoIdOnError); + if (photoIndex > -1) { + // Вместо удаления можно установить флаг ошибки или заменить URL на заглушку + // userInSuggestions.photos.splice(photoIndex, 1); + // Если просто убрать фото из массива, и оно было единственным, карусель исчезнет. + // Лучше обработать это в UI, показав заглушку для этого конкретного слайда или скрыв слайд. + // Пока просто выводим ошибку и позволяем Bootstrap обработать битую картинку (покажет alt текст). + event.target.style.display = 'none'; // Скрываем битое изображение + // Можно добавить заглушку на место битого изображения, если это один слайд из многих + } + // Если после ошибки не осталось фото, показываем общую заглушку + if (!userInSuggestions.photos.some(p => p.url)) { + userInSuggestions.mainPhotoUrl = null; // Это для старой логики с одним фото, нужно будет обновить + } } - // event.target.style.display = 'none'; // Можно убрать, v-else должен справиться }; @@ -292,6 +327,10 @@ const onImageError = (event, userOnError) => { height: 100%; object-fit: cover; } +.carousel-item img { + height: 400px; /* Фиксированная высота для фото в карусели */ + object-fit: cover; +} .no-photo-placeholder { border-bottom: 1px solid #dee2e6; /* Граница, как у card-img-top */ }