Files
front/src/components/General_section.vue
admin 855c06d313
All checks were successful
continuous-integration/drone/push Build is passing
изменил правила хранения промтов
2026-04-11 14:05:25 +10:00

311 lines
8.5 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.
<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 ref="scrollContainer" class="p-4">
<Stat
v-for="item in items"
:key="item.url"
: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"
:original_text="item.original_text"
:translation_text="item.translation_text"
:other="item.other"
@update:viewed="handleViewedChange"
@update:status="handleStatusChange"
/>
<!-- 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, onUnmounted, watch, nextTick } from "vue";
import Stat from "./One_kard.vue";
import Time from "./Time.vue";
import axios from "axios";
const props = defineProps({
filter: { type: String, default: "all" },
});
// Константы
const LIMIT = 50;
const POLL_INTERVAL = 15000; // 15 секунд
// Состояния
const isDarkMode = ref(document.documentElement.classList.contains("dark"));
const sentinel = ref(null);
const scrollContainer = ref(null); // Реф контейнера скролла
const isLoading = ref(false);
const hasMore = ref(true);
const items = ref([]);
const poisk = ref("");
// Пагинация
let currentFilter = "default";
let currentOffset = 0;
let pollTimer = null;
let lastScrollTop = 0; // Сохраняем позицию скролла
const fetchData = async (url) => {
try {
const { data } = await axios.get(url);
return data;
} catch (err) {
console.error(`Ошибка при получении данных:`, err);
return [];
}
};
const fetchTotalCount = async (filterValue) => {
try {
const { data } = await axios.get(
`https://allowlgroup.ru/api/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(
`https://allowlgroup.ru/api/8002/poisk/count?query=${query}&item=${filterValue}`,
);
return data.count;
} catch (err) {
console.error("Ошибка при получении количества:", err);
return 0;
}
};
// === Загрузка данных ===
const loadItems = async (filterValue, append = false) => {
if (isLoading.value) return;
isLoading.value = true;
if (!append) {
items.value = [];
currentOffset = 0;
hasMore.value = true;
}
currentFilter = filterValue;
try {
const url = poisk.value.trim()
? `https://allowlgroup.ru/api/8002/poisk?query=${poisk.value}&item=${filterValue}&offset=${currentOffset}&limit=${LIMIT}`
: `https://allowlgroup.ru/api/8002/records_all?item=${filterValue}&offset=${currentOffset}&limit=${LIMIT}`;
const totalCount = poisk.value.trim()
? await fetchSearchCount(poisk.value, filterValue)
: await fetchTotalCount(filterValue);
const data = await fetchData(url);
if (append) {
items.value = [...items.value, ...data];
} else {
items.value = data;
}
currentOffset += LIMIT;
hasMore.value = currentOffset < totalCount;
} finally {
isLoading.value = false;
}
};
// === Polling - проверка новых данных ===
const checkForUpdates = async () => {
// Не проверяем при активном поиске или загрузке
if (isLoading.value || (poisk.value && poisk.value.trim())) {
return;
}
try {
const totalCount = await fetchTotalCount(currentFilter);
const data = await fetchData(
`https://allowlgroup.ru/api/8002/records_all?item=${currentFilter}&offset=0&limit=${LIMIT}`,
);
if (!data.length) return;
// Создаём Map существующих URL для быстрого поиска
const existingUrls = new Map(items.value.map((item) => [item.url, item]));
const newItems = [];
let hasNew = false;
for (const item of data) {
const existing = existingUrls.get(item.url);
if (!existing) {
// Новая запись - добавляем в начало
newItems.push(item);
hasNew = true;
} else if (
existing.viewed !== item.viewed ||
existing.status !== item.status
) {
// Изменились viewed/status - обновляем
const index = items.value.indexOf(existing);
items.value[index] = { ...item };
hasNew = true;
}
}
// Добавляем новые записи в начало
if (newItems.length > 0) {
items.value = [...newItems, ...items.value];
}
} catch (err) {
console.error("Ошибка при проверке обновлений:", err);
}
};
const startPolling = () => {
stopPolling();
pollTimer = setInterval(checkForUpdates, POLL_INTERVAL);
};
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
};
// === Обработчики событий от Stat ===
const handleViewedChange = ({ url, viewed }) => {
const item = items.value.find((i) => i.url === url);
if (item) item.viewed = viewed;
};
const handleStatusChange = ({ url, status }) => {
const item = items.value.find((i) => i.url === url);
if (item) item.status = status;
};
// === Watch ===
let debounceTimer = null;
watch(poisk, (newVal) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
currentOffset = 0;
loadItems(currentFilter);
}, 800);
});
watch(
() => props.filter,
(newFilter) => {
const filterValue = newFilter === "all" ? "default" : newFilter;
currentFilter = filterValue;
poisk.value = "";
loadItems(filterValue);
},
);
const onfilterItems = (filterValue) => {
const filter = filterValue === "all" ? "default" : filterValue;
loadItems(filter);
};
// === Lifecycle ===
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);
// Запускаем polling
startPolling();
// Observer для бесконечного скролла
nextTick(() => {
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value) {
loadItems(currentFilter, true); // append = true
}
},
{ rootMargin: "100px" },
);
if (sentinel.value) {
observer.observe(sentinel.value);
}
});
});
onUnmounted(() => {
stopPolling();
if (observer) observer.disconnect();
});
</script>