фотографии профиля
This commit is contained in:
parent
58f6df7d69
commit
1e0c1dc9b4
@ -194,6 +194,7 @@ const uploadUserProfilePhoto = async (req, res, next) => {
|
|||||||
const newPhoto = {
|
const newPhoto = {
|
||||||
url: result.secure_url,
|
url: result.secure_url,
|
||||||
public_id: result.public_id,
|
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();
|
await user.save();
|
||||||
|
|
||||||
// Получаем обновленного пользователя с обновленным массивом фотографий
|
// Получаем обновленного пользователя с обновленным массивом фотографий
|
||||||
const updatedUser = await User.findById(req.user._id);
|
const updatedUser = await User.findById(req.user._id).lean(); // .lean() для простого объекта
|
||||||
|
|
||||||
// Важно! Явно создаем массив allPhotos для ответа, чтобы убедиться, что структура каждого объекта фото совпадает
|
// Важно! Явно создаем массив allPhotos для ответа, чтобы убедиться, что структура каждого объекта фото совпадает
|
||||||
const allPhotos = updatedUser.photos.map(photo => ({
|
const allPhotos = updatedUser.photos.map(photo => ({
|
||||||
|
_id: photo._id, // Добавляем _id для фото
|
||||||
url: photo.url,
|
url: photo.url,
|
||||||
public_id: photo.public_id,
|
public_id: photo.public_id,
|
||||||
isProfilePhoto: photo.isProfilePhoto
|
isProfilePhoto: photo.isProfilePhoto
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Найдем только что добавленное фото в обновленном массиве, чтобы вернуть его с _id
|
||||||
|
const addedPhotoWithId = allPhotos.find(p => p.public_id === newPhoto.public_id);
|
||||||
|
|
||||||
// Возвращаем ответ с точной структурой, ожидаемой тестами
|
// Возвращаем ответ с точной структурой, ожидаемой тестами
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'Фотография успешно загружена!',
|
message: 'Фотография успешно загружена!',
|
||||||
photo: newPhoto,
|
photo: addedPhotoWithId, // Возвращаем фото с _id
|
||||||
allPhotos: allPhotos
|
allPhotos: allPhotos
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -227,12 +232,115 @@ const uploadUserProfilePhoto = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Другие возможные функции для userController (например, getUserById, getAllUsers для админа и т.д.)
|
// @desc Установить фотографию как главную
|
||||||
// const getUserById = async (req, res, next) => { ... };
|
// @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 = {
|
module.exports = {
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
getUsersForSwiping,
|
getUsersForSwiping,
|
||||||
uploadUserProfilePhoto,
|
uploadUserProfilePhoto,
|
||||||
|
setMainPhoto, // <--- Добавлено
|
||||||
|
deletePhoto, // <--- Добавлено
|
||||||
// getUserById,
|
// getUserById,
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
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 { protect } = require('../middleware/authMiddleware'); // Нам нужен protect для защиты маршрута
|
||||||
const multer = require('multer'); // 1. Импортируем multer
|
const multer = require('multer'); // 1. Импортируем multer
|
||||||
const path = require('path'); // Может понадобиться для фильтрации файлов
|
const path = require('path'); // Может понадобиться для фильтрации файлов
|
||||||
@ -39,6 +39,8 @@ router.get('/suggestions', protect, getUsersForSwiping); // <--- НОВЫЙ МА
|
|||||||
// Маршрут для загрузки фотографии профиля
|
// Маршрут для загрузки фотографии профиля
|
||||||
// POST /api/users/profile/photo
|
// POST /api/users/profile/photo
|
||||||
router.post('/profile/photo', protect, upload.single('profilePhoto'), uploadUserProfilePhoto);
|
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 (например, для просмотра чужих профилей, если это нужно)
|
// Маршрут для получения профиля по ID (например, для просмотра чужих профилей, если это нужно)
|
||||||
// GET /api/users/:id
|
// GET /api/users/:id
|
||||||
|
45
package-lock.json
generated
45
package-lock.json
generated
@ -12,7 +12,9 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.6",
|
||||||
|
"browser-image-compression": "^2.0.2",
|
||||||
"cloudinary": "^2.6.1",
|
"cloudinary": "^2.6.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@ -2816,6 +2818,12 @@
|
|||||||
"bcrypt": "bin/bcrypt"
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
@ -2866,6 +2874,15 @@
|
|||||||
"concat-map": "0.0.1"
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.5",
|
"version": "4.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||||
@ -3088,6 +3105,16 @@
|
|||||||
"node": ">=4.0.0"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -4343,6 +4370,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/is-boolean-object": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
|
||||||
@ -6683,6 +6722,12 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.6",
|
||||||
|
"browser-image-compression": "^2.0.2",
|
||||||
"cloudinary": "^2.6.1",
|
"cloudinary": "^2.6.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
@ -37,10 +37,10 @@
|
|||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<h4>Загрузить фото профиля</h4>
|
<h4>Загрузить фото профиля</h4>
|
||||||
<form @submit.prevent="handlePhotoUpload" class="mt-3">
|
<form @submit.prevent="handleMultiplePhotoUpload" class="mt-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="profilePhoto" class="form-label">Выберите фото:</label>
|
<label for="profilePhotos" class="form-label">Выберите фото:</label>
|
||||||
<input type="file" class="form-control" id="profilePhoto" @change="onFileSelected" accept="image/*" :disabled="photoLoading">
|
<input type="file" class="form-control" id="profilePhotos" @change="onFilesSelected" accept="image/*" multiple :disabled="photoLoading">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="previewUrl" class="mb-3">
|
<div v-if="previewUrl" class="mb-3">
|
||||||
@ -51,7 +51,11 @@
|
|||||||
<div v-if="photoUploadSuccessMessage" class="alert alert-success">{{ photoUploadSuccessMessage }}</div>
|
<div v-if="photoUploadSuccessMessage" class="alert alert-success">{{ photoUploadSuccessMessage }}</div>
|
||||||
<div v-if="photoUploadErrorMessage" class="alert alert-danger">{{ photoUploadErrorMessage }}</div>
|
<div v-if="photoUploadErrorMessage" class="alert alert-danger">{{ photoUploadErrorMessage }}</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-secondary" :disabled="photoLoading || !selectedFile">
|
<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">
|
||||||
<span v-if="photoLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span v-if="photoLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
{{ photoLoading ? 'Загрузка...' : 'Загрузить фото' }}
|
{{ photoLoading ? 'Загрузка...' : 'Загрузить фото' }}
|
||||||
</button>
|
</button>
|
||||||
@ -63,6 +67,7 @@
|
|||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import { useAuth } from '@/auth';
|
import { useAuth } from '@/auth';
|
||||||
import api from '@/services/api';
|
import api from '@/services/api';
|
||||||
|
import imageCompression from 'browser-image-compression'; // Импортируем библиотеку
|
||||||
|
|
||||||
const { user, fetchUser } = useAuth();
|
const { user, fetchUser } = useAuth();
|
||||||
|
|
||||||
@ -80,8 +85,12 @@
|
|||||||
const photoLoading = ref(false);
|
const photoLoading = ref(false);
|
||||||
const photoUploadErrorMessage = ref('');
|
const photoUploadErrorMessage = ref('');
|
||||||
const photoUploadSuccessMessage = ref('');
|
const photoUploadSuccessMessage = ref('');
|
||||||
const selectedFile = ref(null);
|
const selectedFile = ref(null); // Это будет использоваться для одиночной загрузки, если мы решим ее оставить
|
||||||
const previewUrl = ref(null);
|
const previewUrl = ref(null); // И это
|
||||||
|
|
||||||
|
// Новые refs для множественной загрузки
|
||||||
|
const selectedFiles = ref([]); // Массив выбранных файлов
|
||||||
|
const photoUploadMessages = ref([]); // Массив сообщений о статусе загрузки каждого файла
|
||||||
|
|
||||||
const initializeFormData = (currentUser) => {
|
const initializeFormData = (currentUser) => {
|
||||||
console.log('[EditProfileForm] Инициализация формы с данными:', currentUser);
|
console.log('[EditProfileForm] Инициализация формы с данными:', currentUser);
|
||||||
@ -90,6 +99,7 @@
|
|||||||
formData.value.bio = currentUser.bio || '';
|
formData.value.bio = currentUser.bio || '';
|
||||||
formData.value.dateOfBirth = currentUser.dateOfBirth ? new Date(currentUser.dateOfBirth).toISOString().split('T')[0] : '';
|
formData.value.dateOfBirth = currentUser.dateOfBirth ? new Date(currentUser.dateOfBirth).toISOString().split('T')[0] : '';
|
||||||
formData.value.gender = currentUser.gender || '';
|
formData.value.gender = currentUser.gender || '';
|
||||||
|
// Не очищаем photoUploadMessages здесь, чтобы пользователь видел результаты предыдущих загрузок в сессии
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,61 +147,101 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFileSelected = (event) => {
|
// Вместо onFileSelected для одного файла, создадим onFilesSelected для нескольких
|
||||||
const file = event.target.files[0];
|
const onFilesSelected = (event) => {
|
||||||
if (file) {
|
const files = event.target.files;
|
||||||
selectedFile.value = file;
|
if (files && files.length > 0) {
|
||||||
// Создание URL для предпросмотра
|
selectedFiles.value = Array.from(files); // Сохраняем массив файлов
|
||||||
const reader = new FileReader();
|
photoUploadMessages.value = selectedFiles.value.map(file => ({ // Инициализируем сообщения для каждого файла
|
||||||
reader.onload = (e) => {
|
name: file.name,
|
||||||
previewUrl.value = e.target.result;
|
status: 'pending', // pending, uploading, success, error
|
||||||
};
|
message: 'Ожидает загрузки...'
|
||||||
reader.readAsDataURL(file);
|
}));
|
||||||
photoUploadErrorMessage.value = ''; // Сброс ошибки при выборе нового файла
|
// Очищаем старые одиночные состояния, если они были
|
||||||
photoUploadSuccessMessage.value = ''; // Сброс сообщения об успехе
|
|
||||||
} else {
|
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
previewUrl.value = null;
|
previewUrl.value = null;
|
||||||
|
photoUploadErrorMessage.value = '';
|
||||||
|
photoUploadSuccessMessage.value = '';
|
||||||
|
} else {
|
||||||
|
selectedFiles.value = [];
|
||||||
|
photoUploadMessages.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhotoUpload = async () => {
|
// Обновляем handlePhotoUpload для загрузки нескольких файлов последовательно
|
||||||
if (!selectedFile.value) {
|
const handleMultiplePhotoUpload = async () => {
|
||||||
photoUploadErrorMessage.value = 'Пожалуйста, выберите файл для загрузки.';
|
if (selectedFiles.value.length === 0) {
|
||||||
|
photoUploadErrorMessage.value = 'Пожалуйста, выберите файлы для загрузки.';
|
||||||
|
// Очистим индивидуальные сообщения, если они были
|
||||||
|
photoUploadMessages.value.forEach(msg => {
|
||||||
|
if (msg.status === 'pending') msg.status = 'cancelled';
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[EditProfileForm] Загрузка фото...');
|
|
||||||
photoLoading.value = true;
|
|
||||||
photoUploadErrorMessage.value = '';
|
|
||||||
photoUploadSuccessMessage.value = '';
|
|
||||||
|
|
||||||
const fd = new FormData();
|
photoLoading.value = true; // Общий индикатор загрузки
|
||||||
fd.append('profilePhoto', selectedFile.value, selectedFile.value.name);
|
photoUploadErrorMessage.value = ''; // Сбрасываем общую ошибку
|
||||||
|
|
||||||
try {
|
for (let i = 0; i < selectedFiles.value.length; i++) {
|
||||||
console.log('[EditProfileForm] Отправка фото на сервер:', selectedFile.value.name);
|
const originalFile = selectedFiles.value[i];
|
||||||
const response = await api.uploadUserProfilePhoto(fd); // Предполагается, что такой метод есть в api.js
|
const fileMessageIndex = photoUploadMessages.value.findIndex(m => m.name === originalFile.name && m.status !== 'success');
|
||||||
console.log('[EditProfileForm] Ответ от сервера (фото):', response.data);
|
|
||||||
|
|
||||||
photoUploadSuccessMessage.value = response.data.message || 'Фото успешно загружено!';
|
if (fileMessageIndex === -1) continue;
|
||||||
selectedFile.value = null;
|
|
||||||
previewUrl.value = null;
|
photoUploadMessages.value[fileMessageIndex].status = 'uploading';
|
||||||
// Очищаем поле выбора файла визуально (браузер может не сбросить его сам)
|
photoUploadMessages.value[fileMessageIndex].message = 'Сжатие и загрузка...';
|
||||||
const photoInput = document.getElementById('profilePhoto');
|
|
||||||
if (photoInput) {
|
try {
|
||||||
photoInput.value = '';
|
// Опции сжатия
|
||||||
|
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; // Выключаем общий индикатор
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -208,4 +258,16 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 0.25rem;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -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() {
|
getUserConversations() {
|
||||||
return apiClient.get('/conversations'); // GET /api/conversations
|
return apiClient.get('/conversations'); // GET /api/conversations
|
||||||
},
|
},
|
||||||
|
@ -25,9 +25,30 @@
|
|||||||
<div v-if="profileData && profileData.photos && profileData.photos.length > 0 && !initialLoading" class="card mb-4">
|
<div v-if="profileData && profileData.photos && profileData.photos.length > 0 && !initialLoading" class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Мои фотографии</h5>
|
<h5 class="card-title">Мои фотографии</h5>
|
||||||
|
<p v-if="photoActionError" class="alert alert-danger">{{ photoActionError }}</p>
|
||||||
|
<p v-if="photoActionSuccess" class="alert alert-success">{{ photoActionSuccess }}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-for="photo in profileData.photos" :key="photo.public_id" class="col-md-4 mb-3">
|
<div v-for="photo in profileData.photos" :key="photo.public_id || photo._id" class="col-md-4 mb-3">
|
||||||
<img :src="photo.url" class="img-fluid img-thumbnail" alt="Фото пользователя">
|
<div class="photo-container position-relative">
|
||||||
|
<img :src="photo.url" class="img-fluid img-thumbnail" alt="Фото пользователя">
|
||||||
|
<div class="photo-actions position-absolute bottom-0 start-0 end-0 p-2 bg-dark bg-opacity-50">
|
||||||
|
<button
|
||||||
|
@click="setAsMainPhoto(photo._id)"
|
||||||
|
class="btn btn-sm btn-light me-1"
|
||||||
|
:disabled="photo.isProfilePhoto || photoActionLoading"
|
||||||
|
title="Сделать главной">
|
||||||
|
<i class="bi" :class="{'bi-star-fill text-warning': photo.isProfilePhoto, 'bi-star': !photo.isProfilePhoto}"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmDeletePhoto(photo._id)"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
:disabled="photoActionLoading"
|
||||||
|
title="Удалить фото">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span v-if="photo.isProfilePhoto" class="badge bg-primary position-absolute top-0 start-0 m-1">Главная</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,6 +65,28 @@
|
|||||||
<p>Не удалось загрузить данные профиля. Возможно, вы не авторизованы или произошла ошибка.</p>
|
<p>Не удалось загрузить данные профиля. Возможно, вы не авторизованы или произошла ошибка.</p>
|
||||||
<router-link to="/login" class="btn btn-primary">Войти</router-link>
|
<router-link to="/login" class="btn btn-primary">Войти</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно подтверждения удаления -->
|
||||||
|
<div class="modal fade" id="deletePhotoModal" tabindex="-1" aria-labelledby="deletePhotoModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deletePhotoModalLabel">Подтвердить удаление</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Вы уверены, что хотите удалить эту фотографию?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="button" class="btn btn-danger" @click="executeDeletePhoto" :disabled="photoActionLoading">
|
||||||
|
<span v-if="photoActionLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -51,15 +94,24 @@
|
|||||||
import { ref, onMounted, computed, watch } from 'vue'; // Добавлен watch
|
import { ref, onMounted, computed, watch } from 'vue'; // Добавлен watch
|
||||||
import { useAuth } from '@/auth';
|
import { useAuth } from '@/auth';
|
||||||
import api from '@/services/api';
|
import api from '@/services/api';
|
||||||
import router from '@/router';
|
// import router from '@/router'; // Закомментировано, так как router не используется напрямую
|
||||||
import EditProfileForm from '@/components/EditProfileForm.vue';
|
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 profileData = ref(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const initialLoading = ref(true);
|
const initialLoading = ref(true);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
|
// Для управления фото
|
||||||
|
const photoActionLoading = ref(false);
|
||||||
|
const photoActionError = ref('');
|
||||||
|
const photoActionSuccess = ref('');
|
||||||
|
const photoToDeleteId = ref(null);
|
||||||
|
let deleteModalInstance = null;
|
||||||
|
|
||||||
|
|
||||||
const fetchProfileDataLocal = async () => {
|
const fetchProfileDataLocal = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
@ -72,10 +124,7 @@ const fetchProfileDataLocal = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Вместо прямого вызова api.getMe(), используем fetchUser из useAuth,
|
|
||||||
// который обновит authUserFromStore, за которым мы будем следить.
|
|
||||||
await fetchUser();
|
await fetchUser();
|
||||||
// profileData.value будет обновляться через watch
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ProfileView] Ошибка при вызове fetchUser:', err);
|
console.error('[ProfileView] Ошибка при вызове fetchUser:', err);
|
||||||
error.value = (err.response && err.response.data && err.response.data.message)
|
error.value = (err.response && err.response.data && err.response.data.message)
|
||||||
@ -87,27 +136,26 @@ const fetchProfileDataLocal = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Следим за изменениями authUserFromStore и обновляем profileData
|
|
||||||
watch(authUserFromStore, (newUser) => {
|
watch(authUserFromStore, (newUser) => {
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
profileData.value = { ...newUser }; // Клонируем, чтобы избежать прямой мутации, если это необходимо
|
profileData.value = { ...newUser };
|
||||||
console.log('[ProfileView] Данные профиля обновлены из authUserFromStore:', profileData.value);
|
console.log('[ProfileView] Данные профиля обновлены из authUserFromStore:', profileData.value);
|
||||||
}
|
}
|
||||||
}, { immediate: true, deep: true }); // immediate: true для инициализации при монтировании
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// Если authUserFromStore уже содержит данные (например, после логина),
|
|
||||||
// они будут установлены через watch immediate: true.
|
|
||||||
// Если нет, или мы хотим принудительно обновить, вызываем fetchProfileDataLocal.
|
|
||||||
if (!authUserFromStore.value || Object.keys(authUserFromStore.value).length === 0) {
|
if (!authUserFromStore.value || Object.keys(authUserFromStore.value).length === 0) {
|
||||||
fetchProfileDataLocal();
|
await fetchProfileDataLocal();
|
||||||
} else {
|
} else {
|
||||||
// Данные уже есть, initialLoading можно установить в false
|
profileData.value = { ...authUserFromStore.value };
|
||||||
// loading также, так как данные уже есть
|
loading.value = false;
|
||||||
profileData.value = { ...authUserFromStore.value };
|
initialLoading.value = false;
|
||||||
loading.value = false;
|
console.log('[ProfileView] Данные профиля уже были в хранилище:', profileData.value);
|
||||||
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' };
|
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||||
return new Date(dateString).toLocaleDateString(undefined, options);
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -132,7 +225,14 @@ const formatDate = (dateString) => {
|
|||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
max-width: 100%; /* Убедимся, что изображение не выходит за рамки колонки */
|
max-width: 100%;
|
||||||
height: auto; /* Сохраняем пропорции */
|
height: auto;
|
||||||
|
}
|
||||||
|
.photo-container .photo-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.photo-container:hover .photo-actions {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -21,19 +21,44 @@
|
|||||||
<div class="col-md-6 col-lg-4">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="card shadow-sm user-card">
|
<div class="card shadow-sm user-card">
|
||||||
|
|
||||||
<!-- === ОТОБРАЖЕНИЕ ФОТО === -->
|
<!-- === ОТОБРАЖЕНИЕ ФОТО (КАРУСЕЛЬ) === -->
|
||||||
<div class="image-placeholder-container">
|
<div v-if="currentUserToSwipe.photos && currentUserToSwipe.photos.length > 0"
|
||||||
<img
|
:id="`carouselUserPhotos-${currentUserToSwipe._id}`"
|
||||||
v-if="currentUserToSwipe.mainPhotoUrl"
|
class="carousel slide"
|
||||||
:src="currentUserToSwipe.mainPhotoUrl"
|
data-bs-ride="false">
|
||||||
class="card-img-top user-photo"
|
<div class="carousel-inner">
|
||||||
:alt="'Фото ' + currentUserToSwipe.name"
|
<div v-for="(photo, index) in currentUserToSwipe.photos"
|
||||||
@error="onImageError($event, currentUserToSwipe)"
|
:key="photo.public_id || photo._id"
|
||||||
/>
|
class="carousel-item"
|
||||||
<!-- Заглушка, если фото не загрузилось ИЛИ если mainPhotoUrl нет -->
|
:class="{ active: photo.isProfilePhoto || (!currentUserToSwipe.photos.some(p => p.isProfilePhoto) && index === 0) }">
|
||||||
<div v-else class="no-photo-placeholder d-flex align-items-center justify-content-center">
|
<img :src="photo.url"
|
||||||
<i class="bi bi-person-fill"></i>
|
class="d-block w-100 user-photo"
|
||||||
|
:alt="'Фото ' + currentUserToSwipe.name + ' ' + (index + 1)"
|
||||||
|
@error="onImageError($event, currentUserToSwipe, photo._id)">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button v-if="currentUserToSwipe.photos.length > 1" class="carousel-control-prev" type="button" :data-bs-target="`#carouselUserPhotos-${currentUserToSwipe._id}`" data-bs-slide="prev">
|
||||||
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Previous</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="currentUserToSwipe.photos.length > 1" class="carousel-control-next" type="button" :data-bs-target="`#carouselUserPhotos-${currentUserToSwipe._id}`" data-bs-slide="next">
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Next</span>
|
||||||
|
</button>
|
||||||
|
<div class="carousel-indicators" v-if="currentUserToSwipe.photos.length > 1">
|
||||||
|
<button v-for="(photo, index) in currentUserToSwipe.photos"
|
||||||
|
:key="`indicator-${photo._id}`"
|
||||||
|
type="button"
|
||||||
|
:data-bs-target="`#carouselUserPhotos-${currentUserToSwipe._id}`"
|
||||||
|
:data-bs-slide-to="index"
|
||||||
|
:class="{ active: photo.isProfilePhoto || (!currentUserToSwipe.photos.some(p => p.isProfilePhoto) && index === 0) }"
|
||||||
|
:aria-current="photo.isProfilePhoto || (!currentUserToSwipe.photos.some(p => p.isProfilePhoto) && index === 0) ? 'true' : 'false'"
|
||||||
|
:aria-label="`Slide ${index + 1}`"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Заглушка, если фото не загрузилось ИЛИ если mainPhotoUrl нет -->
|
||||||
|
<div v-else class="no-photo-placeholder d-flex align-items-center justify-content-center">
|
||||||
|
<i class="bi bi-person-fill"></i>
|
||||||
</div>
|
</div>
|
||||||
<!-- ========================== -->
|
<!-- ========================== -->
|
||||||
|
|
||||||
@ -197,15 +222,25 @@ const formatGender = (gender) => {
|
|||||||
return ''; // Возвращаем пустую строку, если пол не указан, чтобы не было "Пол не указан"
|
return ''; // Возвращаем пустую строку, если пол не указан, чтобы не было "Пол не указан"
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImageError = (event, userOnError) => {
|
const onImageError = (event, userOnError, photoIdOnError) => {
|
||||||
console.warn("Не удалось загрузить изображение:", event.target.src, "для пользователя:", userOnError.name);
|
console.warn("Не удалось загрузить изображение:", event.target.src, "для пользователя:", userOnError.name, "фото ID:", photoIdOnError);
|
||||||
// Найдем пользователя в suggestions и установим mainPhotoUrl в null
|
|
||||||
// чтобы v-else сработал корректно и реактивно
|
|
||||||
const userInSuggestions = suggestions.value.find(u => u._id === userOnError._id);
|
const userInSuggestions = suggestions.value.find(u => u._id === userOnError._id);
|
||||||
if (userInSuggestions) {
|
if (userInSuggestions && userInSuggestions.photos) {
|
||||||
userInSuggestions.mainPhotoUrl = null;
|
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 должен справиться
|
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -292,6 +327,10 @@ const onImageError = (event, userOnError) => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
.carousel-item img {
|
||||||
|
height: 400px; /* Фиксированная высота для фото в карусели */
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
.no-photo-placeholder {
|
.no-photo-placeholder {
|
||||||
border-bottom: 1px solid #dee2e6; /* Граница, как у card-img-top */
|
border-bottom: 1px solid #dee2e6; /* Граница, как у card-img-top */
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user