фикс списка городов и их поиск

This commit is contained in:
Professional 2025-05-22 01:09:12 +07:00
parent 1977b169df
commit 09b11b9c4b

View File

@ -25,31 +25,50 @@
</select> </select>
</div> </div>
<!-- Поле для выбора города --> <!-- Улучшенное поле для выбора города -->
<div class="mb-3"> <div class="mb-3 city-select-container">
<label for="editCity" class="form-label">Город:</label> <label for="editCity" class="form-label">Город:</label>
<div class="input-group">
<input <input
type="text" type="text"
class="form-control" class="form-control"
id="editCity" id="editCity"
v-model="citySearchQuery" v-model="citySearchQuery"
placeholder="Начните вводить город..." placeholder="Начните вводить название города..."
@focus="showCityList = true" @focus="showCityList = true"
@input="onCitySearch"
:disabled="profileLoading" :disabled="profileLoading"
/> />
<ul v-if="showCityList && filteredCities.length" class="list-group city-dropdown" style="max-height: 200px; overflow-y: auto; position: absolute; z-index: 1000; width: calc(100% - 2rem);"> <span class="input-group-text" @click="clearCitySelection" v-if="formData.location.city">
<li <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" v-for="city in filteredCities"
:key="city.name" :key="city.name"
class="list-group-item list-group-item-action" class="city-item"
@click="selectCity(city.name)" @click="selectCity(city)"
> >
{{ city.name }} <div class="city-name">{{ city.name }}</div>
</li> <div class="city-region">{{ city.subject }}</div>
</ul> </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" /> <input type="hidden" v-model="formData.location.city" />
</div> </div>
<!-- Конец поля для выбора города --> <!-- Конец улучшенного поля для выбора города -->
<div v-if="profileSuccessMessage" class="alert alert-success">{{ profileSuccessMessage }}</div> <div v-if="profileSuccessMessage" class="alert alert-success">{{ profileSuccessMessage }}</div>
<div v-if="profileErrorMessage" class="alert alert-danger">{{ profileErrorMessage }}</div> <div v-if="profileErrorMessage" class="alert alert-danger">{{ profileErrorMessage }}</div>
@ -90,7 +109,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, computed } from 'vue'; import { ref, onMounted, watch, computed, nextTick } 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'; import imageCompression from 'browser-image-compression';
@ -103,16 +122,18 @@
bio: '', bio: '',
dateOfBirth: '', dateOfBirth: '',
gender: '', gender: '',
location: { // Добавляем location location: {
city: '', city: '',
country: 'Россия' // Можно установить по умолчанию, если применимо country: 'Россия'
} }
}); });
// Для выбора города // Для выбора города
const allCities = ref(russianCities); const allCities = ref([]);
const citySearchQuery = ref(''); const citySearchQuery = ref('');
const showCityList = ref(false); const showCityList = ref(false);
const selectedCity = ref(null);
const searchTimeout = ref(null);
const profileLoading = ref(false); const profileLoading = ref(false);
const profileErrorMessage = ref(''); const profileErrorMessage = ref('');
@ -128,31 +149,82 @@
const selectedFiles = ref([]); // Массив выбранных файлов const selectedFiles = ref([]); // Массив выбранных файлов
const photoUploadMessages = ref([]); // Массив сообщений о статусе загрузки каждого файла const photoUploadMessages = ref([]); // Массив сообщений о статусе загрузки каждого файла
// Улучшенный поиск с дебаунсингом
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);
};
const filteredCities = computed(() => { const filteredCities = computed(() => {
if (!citySearchQuery.value) { if (!citySearchQuery.value) {
return allCities.value; return allCities.value.slice(0, 10); // Возвращаем первые 10 городов, если поле пустое
} }
const query = citySearchQuery.value.toLowerCase();
return allCities.value.filter(city => 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) &&
city.name.toLowerCase().includes(query) city.name.toLowerCase().includes(query)
); );
// Объединяем результаты, сначала точные совпадения, затем частичные
return [...startsWithMatch, ...containsMatch].slice(0, 15);
}); });
const selectCity = (cityName) => { const selectCity = (city) => {
formData.value.location.city = cityName; selectedCity.value = city;
citySearchQuery.value = cityName; // Обновляем текстовое поле, чтобы показать выбранный город formData.value.location.city = city.name;
citySearchQuery.value = city.name;
showCityList.value = false; showCityList.value = false;
}; };
// Закрытие списка городов при клике вне его const clearCitySelection = () => {
selectedCity.value = null;
formData.value.location.city = '';
citySearchQuery.value = '';
};
// Улучшенная логика обработки кликов вне списка
const handleClickOutsideCityList = (event) => { const handleClickOutsideCityList = (event) => {
const cityInput = document.getElementById('editCity'); const cityInput = document.getElementById('editCity');
const cityDropdown = document.querySelector('.city-dropdown'); const cityDropdown = document.querySelector('.city-dropdown');
if (
showCityList.value && if (!cityInput || !event.target) return;
cityInput && !cityInput.contains(event.target) &&
cityDropdown && !cityDropdown.contains(event.target) 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;
}
showCityList.value = false; showCityList.value = false;
} }
}; };
@ -164,21 +236,33 @@
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 || '';
if (currentUser.location) { if (currentUser.location) {
formData.value.location.city = currentUser.location.city || ''; formData.value.location.city = currentUser.location.city || '';
citySearchQuery.value = currentUser.location.city || ''; // Инициализируем и поле поиска citySearchQuery.value = currentUser.location.city || '';
formData.value.location.country = currentUser.location.country || 'Россия'; formData.value.location.country = currentUser.location.country || 'Россия';
} else {
formData.value.location.city = ''; // Если есть выбранный город, находим его в списке
citySearchQuery.value = ''; if (currentUser.location.city) {
formData.value.location.country = 'Россия'; selectedCity.value = allCities.value.find(city =>
city.name === currentUser.location.city
) || null;
}
} }
} }
}; };
onMounted(() => { onMounted(async () => {
console.log('[EditProfileForm] Компонент смонтирован'); console.log('[EditProfileForm] Компонент смонтирован');
// Загружаем список городов и сортируем по имени
allCities.value = russianCities.sort((a, b) => a.name.localeCompare(b.name));
// Инициализируем данные формы
nextTick(() => {
initializeFormData(user.value); initializeFormData(user.value);
});
document.addEventListener('click', handleClickOutsideCityList); document.addEventListener('click', handleClickOutsideCityList);
}); });
@ -205,7 +289,7 @@
bio: formData.value.bio, bio: formData.value.bio,
dateOfBirth: formData.value.dateOfBirth || null, dateOfBirth: formData.value.dateOfBirth || null,
gender: formData.value.gender || null, gender: formData.value.gender || null,
location: { // Включаем location в отправляемые данные location: {
city: formData.value.location.city, city: formData.value.location.city,
country: formData.value.location.country country: formData.value.location.country
} }
@ -330,47 +414,101 @@
</script> </script>
<style scoped> <style scoped>
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
.edit-profile-form { .edit-profile-form {
background-color: #f8f9fa; background-color: #f8f9fa;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
position: relative; /* Для позиционирования выпадающего списка городов */
} }
.img-thumbnail {
border: 1px solid #dee2e6; /* Стили для выбора города */
padding: 0.25rem; .city-select-container {
background-color: #fff; position: relative;
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;
} }
.city-dropdown { .city-dropdown {
position: absolute; position: absolute;
top: 100%; /* Располагаем под инпутом */ top: 100%;
left: 0; left: 0;
right: 0; /* Растягиваем по ширине родителя (или инпута) */ right: 0;
z-index: 1000; z-index: 1000;
border: 1px solid #ced4da; background-color: white;
border-top: none; /* Убираем верхнюю границу, т.к. она будет от инпута */ border: 1px solid #dee2e6;
max-height: 200px; 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;
}
.search-results-info {
padding: 8px 12px;
font-size: 0.9em;
color: #6c757d;
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.city-list-container {
overflow-y: auto; overflow-y: auto;
background-color: white; /* Чтобы список был поверх других элементов */ max-height: 300px;
padding: 5px 0;
} }
.city-dropdown .list-group-item {
.city-item {
padding: 8px 15px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
} }
.city-dropdown .list-group-item:hover {
.city-item:hover {
background-color: #e9ecef;
}
.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;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
}
/* Кнопка очистки */
.input-group-text {
cursor: pointer;
background-color: transparent;
}
.input-group-text:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
/* ...existing code... */
</style> </style>