Files
front/src/components/Section.vue
2026-03-14 10:30:22 +10:00

269 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- AdminLogin.vue -->
<template>
<div class="w-full sm:w-4/5 dark:text-neutral-300">
<div class="bg-white flex justify-between p-3 lg:p-5 dark:bg-gray-800">
<div class="flex flex-col md:flex-row">
<div class="relative">
<img
v-if="isDarkMode"
src="https://img.icons8.com/?size=100&id=WwWusvLMTFd7&format=png&color=000000"
class="absolute top-4 left-3 h-5"
/>
<img
v-else
src="https://img.icons8.com/?size=100&id=zR5EBMqZTIBz&format=png&color=000000"
class="absolute top-4 left-3 h-5"
/>
<input
v-model="poisk"
type="text"
placeholder="Поиск..."
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
/>
</div>
<select
@change="onfilterItems($event.target.value)"
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3 mt-3 md:mt-0 md:ml-4"
>
<option value="all">Все</option>
<option value="time">По времени</option>
<option value="viewed">Просмотренные</option>
<option value="status">Избранные</option>
</select>
</div>
<Time />
</div>
<div class="p-4">
<Stat
v-for="item in items"
:key="item.url"
:translation_text="item.translation_text"
:original_text="item.original_text"
:url="item.url"
:title="item.title"
:article_date="item.article_date"
:short_text="item.short_text"
:category="item.category"
:parsed_at="item.parsed_at"
:status="item.status"
:viewed="item.viewed"
:other="item.other"
/>
<!-- Sentinel для бесконечного скролла -->
<div ref="sentinel" class="h-4"></div>
<div v-if="isLoading" class="text-center p-4">Загрузка...</div>
<div v-if="!hasMore && items.length > 0" class="text-center p-4">
Все загружено
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from "vue";
import Stat from "./Stat.vue";
import Time from "./Time.vue";
import axios from "axios";
const isDarkMode = ref(document.documentElement.classList.contains("dark"));
const sentinel = ref(null);
const isLoading = ref(false);
const hasMore = ref(true);
// Состояния для пагинации
let currentFilter = "default";
let currentOffset = 0;
const LIMIT = 50;
// Состояния страницы и данные для входа
const items = ref([]);
const poisk = ref("");
// Универсальная функция для получения данных
const fetchData = async (url) => {
try {
const { data } = await axios.get(url);
return data;
} catch (err) {
console.error(`Ошибка при получении данных с ${url}:`, err);
return [];
}
};
// Получение общего количества записей
const fetchTotalCount = async (filterValue) => {
try {
const { data } = await axios.get(
`http://45.129.78.228:8002/records_all/count?item=${filterValue}`,
);
return data.count;
} catch (err) {
console.error("Ошибка при получении количества:", err);
return 0;
}
};
const fetchSearchCount = async (query, filterValue) => {
try {
const { data } = await axios.get(
`http://45.129.78.228:8002/poisk/count?query=${query}&item=${filterValue}`,
);
return data.count;
} catch (err) {
console.error("Ошибка при получении количества:", err);
return 0;
}
};
// Начальная загрузка (первые 50 записей)
const loadItems = async (filterValue) => {
if (isLoading.value) return;
isLoading.value = true;
items.value = [];
currentFilter = filterValue;
currentOffset = 0;
hasMore.value = true;
try {
const totalCount = await fetchTotalCount(filterValue);
const data = await fetchData(
`http://45.129.78.228:8002/records_all?item=${filterValue}&offset=0&limit=${LIMIT}`,
);
items.value = data;
currentOffset = LIMIT;
hasMore.value = currentOffset < totalCount;
} finally {
isLoading.value = false;
}
};
// Подгрузка следующих записей (бесконечный скролл)
const loadMore = async () => {
if (isLoading.value || !hasMore.value) return;
isLoading.value = true;
try {
// Если есть поисковый запрос - используем поиск с фильтром
if (poisk.value.trim()) {
const totalCount = await fetchSearchCount(poisk.value, currentFilter);
const data = await fetchData(
`http://45.129.78.228:8002/poisk?query=${poisk.value}&item=${currentFilter}&offset=${currentOffset}&limit=${LIMIT}`,
);
items.value = [...items.value, ...data];
currentOffset += LIMIT;
hasMore.value = currentOffset < totalCount;
} else {
// Иначе обычная загрузка с фильтром
const totalCount = await fetchTotalCount(currentFilter);
const data = await fetchData(
`http://45.129.78.228:8002/records_all?item=${currentFilter}&offset=${currentOffset}&limit=${LIMIT}`,
);
items.value = [...items.value, ...data];
currentOffset += LIMIT;
hasMore.value = currentOffset < totalCount;
}
} finally {
isLoading.value = false;
}
};
// Поиск с пагинацией
const searchItems = async (query, filterValue, loadMoreFlag = false) => {
if (isLoading.value && !loadMoreFlag) return;
if (!loadMoreFlag) {
items.value = [];
currentOffset = 0;
hasMore.value = true;
}
isLoading.value = true;
try {
const totalCount = await fetchSearchCount(query, filterValue);
const data = await fetchData(
`http://45.129.78.228:8002/poisk?query=${query}&item=${filterValue}&offset=${currentOffset}&limit=${LIMIT}`,
);
if (loadMoreFlag) {
items.value = [...items.value, ...data];
} else {
items.value = data;
}
currentOffset += LIMIT;
hasMore.value = currentOffset < totalCount;
} finally {
isLoading.value = false;
}
};
// Debounce для поиска
let debounceTimer = null;
watch(poisk, async (newVal) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
if (newVal && newVal.trim()) {
currentOffset = 0;
await searchItems(newVal, currentFilter);
} else {
// При пустом поиске - загружаем все записи
await loadItems(currentFilter);
}
}, 800);
});
const onfilterItems = (filterValue) => {
const filter = filterValue === "all" ? "default" : filterValue;
currentFilter = filter;
loadItems(filter);
};
const props = defineProps({
filter: {
type: String,
default: "all",
},
});
// Observer для бесконечного скролла
let observer = null;
onMounted(() => {
// Инициализация при загрузке
isDarkMode.value = document.documentElement.classList.contains("dark");
const mutationObserver = new MutationObserver(() => {
isDarkMode.value = document.documentElement.classList.contains("dark");
});
mutationObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
// Загружаем данные при монтировании
const initialFilter = props.filter === "all" ? "default" : props.filter;
currentFilter = initialFilter;
loadItems(initialFilter);
// Настройка Intersection Observer для бесконечного скролла
nextTick(() => {
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value) {
loadMore();
}
},
{ rootMargin: "100px" },
);
if (sentinel.value) {
observer.observe(sentinel.value);
}
});
});
</script>