полный рефакторинг всей системы
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
70
src/App.vue
70
src/App.vue
@@ -1,66 +1,28 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { ref } from 'vue'
|
||||||
import All_new_section from "./components/Naw_bar/All_new_section.vue";
|
import NavBar from './components/Naw_bar/All_new_section.vue'
|
||||||
import General_section from "./components/News_section/General_section.vue";
|
import NewsFeed from './components/News_section/General_section.vue'
|
||||||
import Setings from "./components/Settings_section/Setings.vue";
|
import Settings from './components/Settings_section/Settings.vue'
|
||||||
import Authe from "./components/Autherization/Authe.vue";
|
import Auth from './components/Autherization/Authe.vue'
|
||||||
import Istochnik from "./components/Istochnik_section/Istochnik.vue";
|
import Sources from './components/Istochnik_section/Istochnik.vue'
|
||||||
|
|
||||||
// Инициализация темы при загрузке приложения
|
const currentPage = ref('admin-panel')
|
||||||
onMounted(() => {
|
|
||||||
const savedTheme = localStorage.getItem("theme");
|
|
||||||
if (savedTheme === "dark") {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
} else if (savedTheme === "light") {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
} else {
|
|
||||||
// Проверяем системные настройки
|
|
||||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Состояния страницы и данные для входа
|
function navigateTo(page) {
|
||||||
const currentPage = ref("admin-panel");
|
currentPage.value = page
|
||||||
|
|
||||||
function handleUpdate(newValue) {
|
|
||||||
currentPage.value = newValue; // изменение значения в родителе
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Authe :currentPage="currentPage" @update="handleUpdate" />
|
<Auth :currentPage="currentPage" @update="navigateTo" />
|
||||||
<div v-if="currentPage === 'rezylt'">
|
|
||||||
<div class="sm:flex">
|
|
||||||
<All_new_section :currentPage="currentPage" @update="handleUpdate" />
|
|
||||||
<General_section filter="all" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="currentPage === 'setings'">
|
<template v-if="currentPage !== 'admin-panel'">
|
||||||
<div class="sm:flex">
|
<div class="sm:flex">
|
||||||
<All_new_section :currentPage="currentPage" @update="handleUpdate" />
|
<NavBar :currentPage="currentPage" @update="navigateTo" />
|
||||||
<Setings />
|
<NewsFeed v-if="currentPage === 'rezylt'" filter="all" />
|
||||||
|
<Settings v-else-if="currentPage === 'setings'" />
|
||||||
|
<Sources v-else-if="currentPage === 'istochnik'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- <div v-if="currentPage === 'status'">
|
|
||||||
<div class="sm:flex">
|
|
||||||
<All_new_section :currentPage="currentPage" @update="handleUpdate" />
|
|
||||||
<General_section filter="status" />
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div v-if="currentPage === 'istochnik'">
|
|
||||||
<div class="sm:flex">
|
|
||||||
<All_new_section :currentPage="currentPage" @update="handleUpdate" />
|
|
||||||
<Istochnik />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,140 +1,93 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
import { ref } from 'vue'
|
||||||
import axios from "axios";
|
import { login as apiLogin } from '@/services/authService.js'
|
||||||
|
|
||||||
// Получение props
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currentPage: String,
|
currentPage: String,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Объявление события для обновления страницы
|
const emit = defineEmits(['update'])
|
||||||
const emit = defineEmits(["update"]);
|
|
||||||
|
|
||||||
const login = ref("");
|
const login = ref('')
|
||||||
const password = ref("");
|
const password = ref('')
|
||||||
const authError = ref(false);
|
const authError = ref(false)
|
||||||
const isLoggedIn = ref(false);
|
const isLoading = ref(false)
|
||||||
|
|
||||||
// // Показать badge reCAPTCHA
|
async function handleLogin() {
|
||||||
// const showRecaptchaBadge = () => {
|
isLoading.value = true
|
||||||
// setTimeout(() => {
|
authError.value = false
|
||||||
// const badge = document.querySelector(".grecaptcha-badge");
|
|
||||||
// if (badge) {
|
|
||||||
// badge.style.display = "block";
|
|
||||||
// }
|
|
||||||
// }, 100);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Скрыть badge reCAPTCHA
|
|
||||||
// const hideRecaptchaBadge = () => {
|
|
||||||
// const badge = document.querySelector(".grecaptcha-badge");
|
|
||||||
// if (badge) {
|
|
||||||
// badge.style.display = "none";
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Функция авторизации
|
|
||||||
const auth_my = async () => {
|
|
||||||
try {
|
try {
|
||||||
// Получение токена reCAPTCHA
|
const data = await apiLogin(login.value, password.value)
|
||||||
// const recaptchaToken = await grecaptcha.execute(
|
if (data.message === 'Login successful') {
|
||||||
// "6LdfSo8sAAAAAGGhbgIGO51nHgMUALYjcAMOxnOg",
|
emit('update', 'rezylt')
|
||||||
// { action: "login" },
|
|
||||||
// );
|
|
||||||
|
|
||||||
const response = await axios.post("https://allowlgroup.ru/api/8004/login", {
|
|
||||||
username: login.value,
|
|
||||||
password: password.value,
|
|
||||||
// recaptcha_token: recaptchaToken,
|
|
||||||
});
|
|
||||||
if (response.data.message === "Login successful") {
|
|
||||||
authError.value = false;
|
|
||||||
isLoggedIn.value = true;
|
|
||||||
// hideRecaptchaBadge();
|
|
||||||
emit("update", "rezylt");
|
|
||||||
} else {
|
} else {
|
||||||
authError.value = true;
|
authError.value = true
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authError.value = true;
|
authError.value = true
|
||||||
console.log(err);
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Обработка глобального нажатия Enter
|
|
||||||
const handleKeyDown = (event) => {
|
|
||||||
if (event.key === "Enter"&& !isLoggedIn.value) {
|
|
||||||
auth_my();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Добавляем глобальный слушатель при монтировании
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
// showRecaptchaBadge();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаляем при размонтировании
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Переход на стартовую страницу (сброс данных)
|
|
||||||
function showStartPage() {
|
|
||||||
emit("update", "admin-panel");
|
|
||||||
login.value = "";
|
|
||||||
password.value = "";
|
|
||||||
authError.value = false;
|
|
||||||
isLoggedIn.value = false;
|
|
||||||
// Показываем badge reCAPTCHA
|
|
||||||
// showRecaptchaBadge();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отслеживание перехода на страницу admin-panel
|
function handleKeyDown(event) {
|
||||||
// watch(
|
if (event.key === 'Enter') {
|
||||||
// () => props.currentPage,
|
handleLogin()
|
||||||
// (newPage) => {
|
}
|
||||||
// if (newPage === "admin-panel" && !isLoggedIn.value) {
|
}
|
||||||
// showRecaptchaBadge();
|
|
||||||
// }
|
function showStartPage() {
|
||||||
// },
|
emit('update', 'admin-panel')
|
||||||
// );
|
login.value = ''
|
||||||
|
password.value = ''
|
||||||
|
authError.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="props.currentPage === 'admin-panel'"
|
v-if="currentPage === 'admin-panel'"
|
||||||
class="p-4 bg-white rounded shadow max-w-md mx-auto mt-10 dark:bg-gray-800 dark:text-neutral-300"
|
class="p-6 bg-white rounded-xl shadow max-w-md mx-auto mt-10 dark:bg-gray-800 dark:text-neutral-300"
|
||||||
>
|
>
|
||||||
<h2>Вход для администратора</h2>
|
<h2 class="text-xl font-semibold mb-4">Вход для администратора</h2>
|
||||||
<div class="mb-2">
|
<div class="mb-3">
|
||||||
<label>Логин:</label>
|
<label class="block mb-1 text-sm">Логин</label>
|
||||||
<input v-model="login" type="text" class="border p-1 w-full" />
|
<input
|
||||||
|
v-model="login"
|
||||||
|
type="text"
|
||||||
|
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-4">
|
||||||
<label>Пароль:</label>
|
<label class="block mb-1 text-sm">Пароль</label>
|
||||||
<input v-model="password" type="password" class="border p-1 w-full" />
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="auth_my"
|
type="button"
|
||||||
class="mr-2 bg-teal-600 hover:bg-teal-800 text-white px-4 py-2 rounded cursor-pointer"
|
:disabled="isLoading"
|
||||||
|
class="mr-2 bg-teal-600 hover:bg-teal-800 text-white px-4 py-2 rounded-lg cursor-pointer disabled:opacity-50"
|
||||||
|
@click="handleLogin"
|
||||||
>
|
>
|
||||||
Войти
|
{{ isLoading ? 'Вход...' : 'Войти' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dark:bg-gray-600 dark:hover:bg-gray-700 bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded-lg cursor-pointer"
|
||||||
@click="showStartPage"
|
@click="showStartPage"
|
||||||
class="dark:bg-gray-600 dark:hover:bg-gray-700 bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded cursor-pointer"
|
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
<div v-if="authError" class="mt-2 text-red-600">
|
<div v-if="authError" class="mt-3 text-red-600 text-sm">
|
||||||
Неверный логин или пароль
|
Неверный логин или пароль
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <style scoped>
|
|
||||||
.grecaptcha-badge {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
</style> -->
|
|
||||||
|
|||||||
@@ -3,28 +3,30 @@
|
|||||||
<div
|
<div
|
||||||
class="bg-white flex flex-col lg:flex-row justify-between items-center p-3 lg:p-5 dark:bg-gray-800 gap-3 lg:gap-4"
|
class="bg-white flex flex-col lg:flex-row justify-between items-center p-3 lg:p-5 dark:bg-gray-800 gap-3 lg:gap-4"
|
||||||
>
|
>
|
||||||
<!-- Блок 1: Поиск + Фильтр (всегда в ряд) -->
|
|
||||||
<div class="flex flex-row w-full lg:w-auto">
|
<div class="flex flex-row w-full lg:w-auto">
|
||||||
<div class="relative w-2/3">
|
<div class="relative w-2/3">
|
||||||
<img
|
<img
|
||||||
v-if="isDarkMode"
|
v-if="isDark"
|
||||||
src="https://img.icons8.com/?size=100&id=WwWusvLMTFd7&format=png&color=000000"
|
src="https://img.icons8.com/?size=100&id=WwWusvLMTFd7&format=png&color=000000"
|
||||||
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
src="https://img.icons8.com/?size=100&id=zR5EBMqZTIBz&format=png&color=000000"
|
src="https://img.icons8.com/?size=100&id=zR5EBMqZTIBz&format=png&color=000000"
|
||||||
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="poisk"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
class="w-full h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
class="w-full h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@change="onfilterItems($event.target.value)"
|
v-model="activeCategory"
|
||||||
|
@change="onFilterChange"
|
||||||
class="w-1/3 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 ml-2"
|
class="w-1/3 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 ml-2"
|
||||||
>
|
>
|
||||||
<option value="all">Все</option>
|
<option value="all">Все</option>
|
||||||
@@ -38,7 +40,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Блок 2: URL + Категория + Кнопка (всегда в ряд) -->
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row w-full lg:w-auto gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-xl border border-gray-200 dark:border-gray-600"
|
class="flex flex-row w-full lg:w-auto gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-xl border border-gray-200 dark:border-gray-600"
|
||||||
>
|
>
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
@click="addSource"
|
@click="addSource"
|
||||||
class="w-20 h-12 bg-green-600 text-white px-2 rounded-xl shadow hover:bg-green-700 cursor-pointer whitespace-nowrap"
|
class="w-20 h-12 bg-green-600 text-white px-2 rounded-xl shadow hover:bg-green-700 cursor-pointer whitespace-nowrap"
|
||||||
>
|
>
|
||||||
@@ -72,10 +74,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Список источников -->
|
|
||||||
<div class="p-4 grid gap-2 grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3">
|
<div class="p-4 grid gap-2 grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3">
|
||||||
<div v-for="source in filteredSources" :key="source.url" class="mb-4 hover:-translate-y-2 hover:shadow-2xl transition">
|
<div
|
||||||
<Istochnik_one_kard
|
v-for="source in filteredSources"
|
||||||
|
:key="source.url"
|
||||||
|
class="mb-4 hover:-translate-y-2 hover:shadow-2xl transition"
|
||||||
|
>
|
||||||
|
<SourceCard
|
||||||
:source="source"
|
:source="source"
|
||||||
@sourceStarted="handleSourceStarted"
|
@sourceStarted="handleSourceStarted"
|
||||||
@sourceDeleted="handleSourceDeleted"
|
@sourceDeleted="handleSourceDeleted"
|
||||||
@@ -86,92 +91,77 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from "vue";
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import axios from "axios";
|
import SourceCard from './Istochnik_one_kard.vue'
|
||||||
import Istochnik_one_kard from "./Istochnik_one_kard.vue";
|
import { useDarkMode } from '@/composables/useDarkMode.js'
|
||||||
|
import { fetchCategories, fetchSources, addSource as apiAddSource } from '@/services/sourceService.js'
|
||||||
|
|
||||||
const isDarkMode = ref(false);
|
const { isDark } = useDarkMode()
|
||||||
const newSourceUrl = ref("");
|
|
||||||
const newSourceCategory = ref("");
|
|
||||||
const categories = ref([]);
|
|
||||||
const poisk = ref("");
|
|
||||||
const sources = ref([]);
|
|
||||||
|
|
||||||
|
const newSourceUrl = ref('')
|
||||||
const fetchCategories = async () => {
|
const newSourceCategory = ref('')
|
||||||
try {
|
const categories = ref([])
|
||||||
const data = await axios.get(
|
const searchQuery = ref('')
|
||||||
"https://allowlgroup.ru/api/8001/categories_promt",
|
const sources = ref([])
|
||||||
);
|
const activeCategory = ref('all')
|
||||||
categories.value = data.data;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Ошибка загрузки категорий:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchSources = async (category) => {
|
|
||||||
try {
|
|
||||||
const data = await axios.get(`https://allowlgroup.ru/api/8001/all_sources`, {
|
|
||||||
params: { category }
|
|
||||||
});
|
|
||||||
sources.value = data.data.sources || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Ошибка загрузки источников:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredSources = computed(() => {
|
const filteredSources = computed(() => {
|
||||||
if (!poisk.value) return sources.value;
|
if (!searchQuery.value) return sources.value
|
||||||
|
const q = searchQuery.value.toLowerCase()
|
||||||
return sources.value.filter(
|
return sources.value.filter(
|
||||||
(source) =>
|
(source) =>
|
||||||
source.url.toLowerCase().includes(poisk.value.toLowerCase()) ||
|
source.url.toLowerCase().includes(q) ||
|
||||||
source.promt.toLowerCase().includes(poisk.value.toLowerCase()),
|
source.promt.toLowerCase().includes(q),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const addSource = async () => {
|
async function loadCategories() {
|
||||||
|
try {
|
||||||
|
categories.value = await fetchCategories()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка загрузки категорий:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSources(category = 'all') {
|
||||||
|
try {
|
||||||
|
sources.value = await fetchSources(category)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка загрузки источников:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSource() {
|
||||||
if (!newSourceUrl.value.trim() || !newSourceCategory.value) {
|
if (!newSourceUrl.value.trim() || !newSourceCategory.value) {
|
||||||
alert("Заполните все поля");
|
alert('Заполните все поля')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post("https://allowlgroup.ru/api/8001/add_sources", {
|
await apiAddSource(newSourceUrl.value, newSourceCategory.value)
|
||||||
url: newSourceUrl.value,
|
newSourceUrl.value = ''
|
||||||
promt: newSourceCategory.value,
|
newSourceCategory.value = ''
|
||||||
});
|
await loadSources(activeCategory.value)
|
||||||
newSourceUrl.value = "";
|
|
||||||
newSourceCategory.value = "";
|
|
||||||
fetchSources(); // Обновляем список после добавления
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка добавления источника:", err);
|
console.error('Ошибка добавления источника:', err)
|
||||||
alert("Ошибка при добавлении источника");
|
alert('Ошибка при добавлении источника')
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const onfilterItems = (filterValue) => {
|
function onFilterChange() {
|
||||||
fetchSources(filterValue);
|
loadSources(activeCategory.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSourceStarted = (url) => {
|
function handleSourceStarted(url) {
|
||||||
console.log("Парсинг запущен для:", url);
|
console.log('Парсинг запущен для:', url)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSourceDeleted = (deletedUrl) => {
|
function handleSourceDeleted(deletedUrl) {
|
||||||
sources.value = sources.value.filter((source) => source.url !== deletedUrl);
|
sources.value = sources.value.filter((source) => source.url !== deletedUrl)
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isDarkMode.value = document.documentElement.classList.contains("dark");
|
loadCategories()
|
||||||
const mutationObserver = new MutationObserver(() => {
|
loadSources('all')
|
||||||
isDarkMode.value = document.documentElement.classList.contains("dark");
|
})
|
||||||
});
|
|
||||||
mutationObserver.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchCategories();
|
|
||||||
fetchSources("all");
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,124 +1,86 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:flex-row justify-between w-full lg:w-auto gap-2 p-2 bg-white shadow dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600"
|
class="flex flex-col sm:flex-row justify-between w-full lg:w-auto gap-2 p-2 bg-white shadow dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600"
|
||||||
:class="{ 'opacity-50': isStatus }"
|
:class="{ 'opacity-50': props.source.status }"
|
||||||
>
|
>
|
||||||
<div class="">
|
<div class="flex-1 flex gap-2 min-w-0">
|
||||||
<input
|
<input
|
||||||
v-model="displayUrl"
|
:value="props.source.url"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="URL источника"
|
placeholder="URL источника"
|
||||||
readonly
|
readonly
|
||||||
class="flex-1 sm:mr-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 w-2/3 sm:min-w-0"
|
class="flex-1 sm:mr-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-model="displayPromt"
|
:value="props.source.promt"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Промт"
|
placeholder="Промт"
|
||||||
readonly
|
readonly
|
||||||
class="flex-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 w-1/3 sm:max-w-20"
|
class="h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 w-1/3 sm:max-w-20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-40">
|
<div class="min-w-40 flex gap-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
@click="deleteParsing"
|
@click="deleteParsing"
|
||||||
:disabled="isDeleting"
|
:disabled="isDeleting"
|
||||||
class="w-1/3 sm:w-9 h-11 sm:mr-1 bg-rose-600 hover:bg-rose-800 shadow text-white rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
class="w-1/3 sm:w-9 h-11 bg-rose-600 hover:bg-rose-800 shadow text-white rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
X
|
×
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
@click="startParsing"
|
@click="startParsing"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
class="w-2/3 sm:w-25 h-11 dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 hover:bg-sky-900 rounded-xl px-2 cursor-pointer whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
class="w-2/3 sm:w-25 h-11 dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 hover:bg-sky-900 rounded-xl px-2 cursor-pointer whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{{ isLoading ? "..." : "Start" }}
|
{{ isLoading ? '...' : 'Start' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref } from 'vue'
|
||||||
import axios from "axios";
|
import { startSourceParsing, deleteSource } from '@/services/sourceService.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
source: {
|
source: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => ({ url: "", promt: "", status: false}),
|
default: () => ({ url: '', promt: '', status: false }),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["sourceStarted", "sourceDeleted"]);
|
const emit = defineEmits(['sourceStarted', 'sourceDeleted'])
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false)
|
||||||
const isDeleting = ref(false);
|
const isDeleting = ref(false)
|
||||||
const isStatus = ref(props.source.status);
|
|
||||||
|
|
||||||
const displayUrl = computed({
|
async function startParsing() {
|
||||||
get: () => props.source.url || "",
|
if (!props.source.url) return
|
||||||
set: (val) => {
|
|
||||||
// Только для чтения
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayPromt = computed({
|
|
||||||
get: () => props.source.promt || "",
|
|
||||||
set: (val) => {
|
|
||||||
// Только для чтения
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const startParsing = async () => {
|
|
||||||
if (!props.source.url) {
|
|
||||||
// alert("URL источника не указан");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
await axios.post("https://allowlgroup.ru/api/8001/parser_all", {
|
await startSourceParsing(props.source.url, props.source.promt)
|
||||||
// await axios.post("http://127.0.0.1:8001/parser_all", {
|
emit('sourceStarted', props.source.url)
|
||||||
url: props.source.url,
|
|
||||||
promt: props.source.promt,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit("sourceStarted", props.source.url);
|
|
||||||
// alert(`Парсинг для ${props.source.url} запущен`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка запуска парсинга:", err);
|
console.error('Ошибка запуска парсинга:', err)
|
||||||
alert("Ошибка при запуске парсинга");
|
alert('Ошибка при запуске парсинга')
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const deleteParsing = async () => {
|
|
||||||
// if (!props.source.url) {
|
|
||||||
// alert("URL источника не указан");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!confirm(`Вы уверены, что хотите удалить источник "${props.source.url}"?`)) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
isDeleting.value = true;
|
|
||||||
|
|
||||||
|
async function deleteParsing() {
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
try {
|
||||||
await axios.delete(
|
await deleteSource(props.source.url)
|
||||||
"https://allowlgroup.ru/api/8001/delete_sources",
|
emit('sourceDeleted', props.source.url)
|
||||||
// "http://127.0.0.1:8001/delete_sources",
|
|
||||||
{ params: { url: props.source.url } }
|
|
||||||
);
|
|
||||||
emit("sourceDeleted", props.source.url);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка при удалении задачи:", error);
|
console.error('Ошибка при удалении источника:', error)
|
||||||
} finally {
|
} finally {
|
||||||
isDeleting.value = false;
|
isDeleting.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,79 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<nav
|
||||||
class="sticky top-0 w-full sm:w-1/5 sm:h-screen bg-white z-10 pt-5 md:p-5 dark:bg-gray-800 flex sm:justify-between flex-col"
|
class="sticky top-0 w-full sm:w-1/5 sm:h-screen bg-white z-10 pt-5 md:p-5 dark:bg-gray-800 flex sm:justify-between flex-col"
|
||||||
>
|
>
|
||||||
<div class="">
|
<div class="flex sm:flex-col gap-2 sm:gap-0">
|
||||||
<div class="flex sm:flex-col">
|
<button
|
||||||
<button
|
v-for="item in navItems"
|
||||||
@click="ValueRezylt"
|
:key="item.page"
|
||||||
:class="getButtonClass('rezylt')"
|
type="button"
|
||||||
>
|
:class="getButtonClass(item.page)"
|
||||||
Результат
|
class="sm:mt-3 first:mt-0"
|
||||||
</button>
|
@click="navigate(item.page)"
|
||||||
|
>
|
||||||
<button
|
{{ item.label }}
|
||||||
@click="ValueSeting"
|
</button>
|
||||||
:class="getButtonClass('setings')"
|
|
||||||
class="sm:mt-3"
|
|
||||||
>
|
|
||||||
Настройка
|
|
||||||
</button>
|
|
||||||
<!--
|
|
||||||
<button
|
|
||||||
@click="Valuestatus"
|
|
||||||
:class="getButtonClass('status')"
|
|
||||||
class="sm:mt-3"
|
|
||||||
>
|
|
||||||
Избранное
|
|
||||||
</button> -->
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="Valueistochnik"
|
|
||||||
:class="getButtonClass('istochnik')"
|
|
||||||
class="sm:mt-3"
|
|
||||||
>
|
|
||||||
Источники
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Переключатель темы -->
|
<div class="hidden sm:block mt-auto">
|
||||||
<div class="hidden sm:block">
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ThemeToggle from "./ThemeToggle.vue";
|
import ThemeToggle from './ThemeToggle.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currentPage: String,
|
currentPage: String,
|
||||||
});
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["update"]);
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ page: 'rezylt', label: 'Результат' },
|
||||||
|
{ page: 'setings', label: 'Настройка' },
|
||||||
|
{ page: 'istochnik', label: 'Источники' },
|
||||||
|
]
|
||||||
|
|
||||||
function getButtonClass(page) {
|
function getButtonClass(page) {
|
||||||
const baseClass = "w-full min-w-30 px-4 py-2 rounded cursor-pointer";
|
const baseClass = 'w-full min-w-30 px-4 py-2 rounded-lg cursor-pointer transition-colors text-white'
|
||||||
const activeClass = "bg-gray-600 text-white";
|
const activeClass = 'bg-gray-600 text-white'
|
||||||
const inactiveClass = "bg-gray-300 hover:text-white hover:bg-gray-600";
|
const inactiveClass = 'bg-gray-300 hover:text-white hover:bg-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600'
|
||||||
|
|
||||||
return `${baseClass} ${props.currentPage === page ? activeClass : inactiveClass}`;
|
return `${baseClass} ${props.currentPage === page ? activeClass : inactiveClass}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function ValueSeting() {
|
function navigate(page) {
|
||||||
emit("update", "setings");
|
emit('update', page)
|
||||||
}
|
|
||||||
|
|
||||||
function ValueRezylt() {
|
|
||||||
emit("update", "rezylt");
|
|
||||||
}
|
|
||||||
|
|
||||||
// function Valuestatus() {
|
|
||||||
// emit("update", "status");
|
|
||||||
// }
|
|
||||||
|
|
||||||
function Valueistochnik() {
|
|
||||||
emit("update", "istochnik");
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,40 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, watch } from 'vue'
|
||||||
|
import { useDarkMode } from '@/composables/useDarkMode.js'
|
||||||
|
|
||||||
const isDark = ref(false);
|
const { isDark } = useDarkMode()
|
||||||
|
const localDark = ref(isDark.value)
|
||||||
|
|
||||||
// Инициализация темы при загрузке
|
watch(isDark, (val) => {
|
||||||
onMounted(() => {
|
localDark.value = val
|
||||||
// Проверяем localStorage
|
})
|
||||||
const savedTheme = localStorage.getItem("theme");
|
|
||||||
if (savedTheme) {
|
function toggleTheme() {
|
||||||
// Если есть сохранённая тема - используем её
|
localDark.value = !localDark.value
|
||||||
isDark.value = savedTheme === "dark";
|
if (localDark.value) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
} else {
|
} else {
|
||||||
// Нет сохранённой темы - берём из системных настроек браузера
|
document.documentElement.classList.remove('dark')
|
||||||
isDark.value = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
||||||
}
|
}
|
||||||
applyTheme();
|
localStorage.setItem('theme', localDark.value ? 'dark' : 'light')
|
||||||
});
|
}
|
||||||
|
|
||||||
// Применение темы
|
|
||||||
const applyTheme = () => {
|
|
||||||
if (isDark.value) {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
localStorage.setItem("theme", isDark.value ? "dark" : "light");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Переключение темы
|
|
||||||
const toggleTheme = () => {
|
|
||||||
isDark.value = !isDark.value;
|
|
||||||
applyTheme();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Следим за изменениями (для реактивности)
|
|
||||||
watch(isDark, applyTheme);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full sm:w-4/5 dark:text-neutral-300">
|
<div class="w-full sm:w-4/5 dark:text-neutral-300">
|
||||||
<div class="bg-white p-3 lg:p-5 dark:bg-gray-800">
|
<div class="bg-white p-3 lg:p-5 dark:bg-gray-800">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between items-start">
|
||||||
<div class="flex flex-col md:flex-row">
|
<div class="flex flex-col md:flex-row gap-3 md:gap-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img
|
<img
|
||||||
v-if="isDarkMode"
|
v-if="isDark"
|
||||||
src="https://img.icons8.com/?size=100&id=WwWusvLMTFd7&format=png&color=000000"
|
src="https://img.icons8.com/?size=100&id=WwWusvLMTFd7&format=png&color=000000"
|
||||||
class="absolute top-4 left-3 h-5"
|
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
src="https://img.icons8.com/?size=100&id=zR5EBMqZTIBz&format=png&color=000000"
|
src="https://img.icons8.com/?size=100&id=zR5EBMqZTIBz&format=png&color=000000"
|
||||||
class="absolute top-4 left-3 h-5"
|
class="absolute top-1/2 -translate-y-1/2 left-3 h-5"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="poisk"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@change="onfilterItems($event.target.value)"
|
v-model="activeFilter"
|
||||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3 mt-3 md:mt-0 md:ml-4"
|
@change="onFilterChange"
|
||||||
|
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3"
|
||||||
>
|
>
|
||||||
<option value="all">Все</option>
|
<option value="all">Все</option>
|
||||||
<option value="time">По времени</option>
|
<option value="time">По времени</option>
|
||||||
@@ -42,38 +45,25 @@
|
|||||||
<Time />
|
<Time />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dark:bg-gray-800 bg-white p-3 hidden lg:block">
|
<div class="dark:bg-gray-800 bg-white p-3 hidden lg:block">
|
||||||
<div class="mx-2 p-3 bg-gray-100 dark:bg-gray-700 rounded-xl border border-gray-200 dark:border-gray-600">
|
<div class="mx-2 p-3 bg-gray-100 dark:bg-gray-700 rounded-xl border border-gray-200 dark:border-gray-600">
|
||||||
<Setings_downloads />
|
<SettingsDownloads />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="scrollContainer" class="p-4">
|
<div ref="scrollContainer" class="p-4">
|
||||||
<Stat
|
<NewsCard
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.url"
|
:key="item.url"
|
||||||
:url="item.url"
|
v-bind="item"
|
||||||
:title="item.title"
|
@update:viewed="handleFieldChange('viewed', $event)"
|
||||||
:article_date="item.article_date"
|
@update:status="handleFieldChange('status', $event)"
|
||||||
:short_text="item.short_text"
|
@update:tematik="handleFieldChange('tematik', $event)"
|
||||||
:category="item.category"
|
@update:svodka="handleFieldChange('svodka', $event)"
|
||||||
:parsed_at="item.parsed_at"
|
@update:donesenie="handleFieldChange('donesenie', $event)"
|
||||||
:status="item.status"
|
@update:bilutene="handleFieldChange('bilutene', $event)"
|
||||||
:viewed="item.viewed"
|
|
||||||
:tematik="item.tematik"
|
|
||||||
:svodka="item.svodka"
|
|
||||||
:donesenie="item.donesenie"
|
|
||||||
:bilutene="item.bilutene"
|
|
||||||
:original_text="item.original_text"
|
|
||||||
:translation_text="item.translation_text"
|
|
||||||
:other="item.other"
|
|
||||||
@update:viewed="handleViewedChange"
|
|
||||||
@update:status="handleStatusChange"
|
|
||||||
@update:tematik="handleTematikChange"
|
|
||||||
@update:svodka="handleSvodkaChange"
|
|
||||||
@update:donesenie="handleDonesenieChange"
|
|
||||||
@update:bilutene="handleBiluteneChange"
|
|
||||||
/>
|
/>
|
||||||
<!-- Sentinel для бесконечного скролла -->
|
|
||||||
<div ref="sentinel" class="h-4"></div>
|
<div ref="sentinel" class="h-4"></div>
|
||||||
<div v-if="isLoading" class="text-center p-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 v-if="!hasMore && items.length > 0" class="text-center p-4">
|
||||||
@@ -84,272 +74,164 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import Stat from "./One_kard.vue";
|
import NewsCard from './NewsCard.vue'
|
||||||
import Time from "./Time.vue";
|
import Time from './Time.vue'
|
||||||
import axios from "axios";
|
import SettingsDownloads from '../Settings_section/SettingsDownloads.vue'
|
||||||
import Setings_downloads from "../Settings_section/Setings_downloads.vue";
|
import { useDarkMode } from '@/composables/useDarkMode.js'
|
||||||
|
import {
|
||||||
|
fetchRecords,
|
||||||
|
fetchRecordsCount,
|
||||||
|
searchRecords,
|
||||||
|
fetchSearchCount,
|
||||||
|
} from '@/services/newsService.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
filter: { type: String, default: "all" },
|
filter: { type: String, default: 'all' },
|
||||||
});
|
})
|
||||||
|
|
||||||
// Константы
|
const LIMIT = 30
|
||||||
const LIMIT = 30;
|
const POLL_INTERVAL = 60 * 60000 // 60 минут
|
||||||
const POLL_INTERVAL = 60 * 60000; // 15 секунд
|
|
||||||
|
|
||||||
// Состояния
|
const { isDark } = useDarkMode()
|
||||||
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("");
|
|
||||||
|
|
||||||
// Пагинация
|
const sentinel = ref(null)
|
||||||
let currentFilter = "default";
|
const scrollContainer = ref(null)
|
||||||
let currentOffset = 0;
|
const isLoading = ref(false)
|
||||||
let pollTimer = null;
|
const hasMore = ref(true)
|
||||||
let lastScrollTop = 0; // Сохраняем позицию скролла
|
const items = ref([])
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const activeFilter = ref('all')
|
||||||
|
|
||||||
const fetchData = async (url) => {
|
let currentFilter = 'default'
|
||||||
try {
|
let currentOffset = 0
|
||||||
const { data } = await axios.get(url);
|
let pollTimer = null
|
||||||
return data;
|
let observer = null
|
||||||
} catch (err) {
|
let debounceTimer = null
|
||||||
console.error(`Ошибка при получении данных:`, err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchTotalCount = async (filterValue) => {
|
async function loadItems(filterValue, append = false) {
|
||||||
try {
|
if (isLoading.value) return
|
||||||
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) => {
|
isLoading.value = true
|
||||||
try {
|
currentFilter = filterValue
|
||||||
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) {
|
if (!append) {
|
||||||
items.value = [];
|
items.value = []
|
||||||
currentOffset = 0;
|
currentOffset = 0
|
||||||
hasMore.value = true;
|
hasMore.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFilter = filterValue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = poisk.value.trim()
|
const isSearch = searchQuery.value.trim().length > 0
|
||||||
? `https://allowlgroup.ru/api/8002/poisk?query=${poisk.value}&item=${filterValue}&offset=${currentOffset}&limit=${LIMIT}`
|
const totalCount = isSearch
|
||||||
: `https://allowlgroup.ru/api/8002/records_all?item=${filterValue}&offset=${currentOffset}&limit=${LIMIT}`;
|
? await fetchSearchCount(searchQuery.value, filterValue)
|
||||||
|
: await fetchRecordsCount(filterValue)
|
||||||
|
|
||||||
const totalCount = poisk.value.trim()
|
const data = isSearch
|
||||||
? await fetchSearchCount(poisk.value, filterValue)
|
? await searchRecords(searchQuery.value, filterValue, currentOffset, LIMIT)
|
||||||
: await fetchTotalCount(filterValue);
|
: await fetchRecords(filterValue, currentOffset, LIMIT)
|
||||||
|
|
||||||
const data = await fetchData(url);
|
items.value = append ? [...items.value, ...data] : data
|
||||||
|
currentOffset += LIMIT
|
||||||
if (append) {
|
hasMore.value = currentOffset < totalCount
|
||||||
items.value = [...items.value, ...data];
|
} catch (err) {
|
||||||
} else {
|
console.error('Ошибка загрузки данных:', err)
|
||||||
items.value = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentOffset += LIMIT;
|
|
||||||
hasMore.value = currentOffset < totalCount;
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// === Polling - проверка новых данных ===
|
async function checkForUpdates() {
|
||||||
|
if (isLoading.value || searchQuery.value.trim()) return
|
||||||
const checkForUpdates = async () => {
|
|
||||||
// Не проверяем при активном поиске или загрузке
|
|
||||||
if (isLoading.value || (poisk.value && poisk.value.trim())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const totalCount = await fetchTotalCount(currentFilter);
|
const data = await fetchRecords(currentFilter, 0, LIMIT)
|
||||||
const data = await fetchData(
|
if (!data.length) return
|
||||||
`https://allowlgroup.ru/api/8002/records_all?item=${currentFilter}&offset=0&limit=${LIMIT}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data.length) return;
|
const existingMap = new Map(items.value.map((item) => [item.url, item]))
|
||||||
|
const newItems = []
|
||||||
// Создаём Map существующих URL для быстрого поиска
|
const changedFields = ['viewed', 'status', 'tematik', 'svodka', 'donesenie', 'bilutene']
|
||||||
const existingUrls = new Map(items.value.map((item) => [item.url, item]));
|
|
||||||
|
|
||||||
const newItems = [];
|
|
||||||
let hasNew = false;
|
|
||||||
|
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
const existing = existingUrls.get(item.url);
|
const existing = existingMap.get(item.url)
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// Новая запись - добавляем в начало
|
newItems.push(item)
|
||||||
newItems.push(item);
|
} else if (changedFields.some((f) => existing[f] !== item[f])) {
|
||||||
hasNew = true;
|
Object.assign(existing, item)
|
||||||
} else if (
|
|
||||||
existing.viewed !== item.viewed ||
|
|
||||||
existing.status !== item.status ||
|
|
||||||
existing.tematik !== item.tematik ||
|
|
||||||
existing.svodka !== item.svodka ||
|
|
||||||
existing.donesenie !== item.donesenie ||
|
|
||||||
existing.bilutene !== item.bilutene
|
|
||||||
) {
|
|
||||||
// Изменились viewed/status - обновляем
|
|
||||||
const index = items.value.indexOf(existing);
|
|
||||||
items.value[index] = { ...item };
|
|
||||||
hasNew = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем новые записи в начало
|
if (newItems.length) {
|
||||||
if (newItems.length > 0) {
|
items.value = [...newItems, ...items.value]
|
||||||
items.value = [...newItems, ...items.value];
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка при проверке обновлений:", err);
|
console.error('Ошибка при проверке обновлений:', err)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const startPolling = () => {
|
function startPolling() {
|
||||||
stopPolling();
|
stopPolling()
|
||||||
pollTimer = setInterval(checkForUpdates, POLL_INTERVAL);
|
pollTimer = setInterval(checkForUpdates, POLL_INTERVAL)
|
||||||
};
|
}
|
||||||
|
|
||||||
const stopPolling = () => {
|
function stopPolling() {
|
||||||
if (pollTimer) {
|
if (pollTimer) {
|
||||||
clearInterval(pollTimer);
|
clearInterval(pollTimer)
|
||||||
pollTimer = null;
|
pollTimer = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// === Обработчики событий от Stat ===
|
function handleFieldChange(field, { url, [field]: value }) {
|
||||||
|
const item = items.value.find((i) => i.url === url)
|
||||||
|
if (item) item[field] = value
|
||||||
|
}
|
||||||
|
|
||||||
const handleViewedChange = ({ url, viewed }) => {
|
function onFilterChange() {
|
||||||
const item = items.value.find((i) => i.url === url);
|
const filter = activeFilter.value === 'all' ? 'default' : activeFilter.value
|
||||||
if (item) item.viewed = viewed;
|
loadItems(filter)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleStatusChange = ({ url, status }) => {
|
watch(searchQuery, () => {
|
||||||
const item = items.value.find((i) => i.url === url);
|
clearTimeout(debounceTimer)
|
||||||
if (item) item.status = status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTematikChange = ({ url, tematik }) => {
|
|
||||||
const item = items.value.find((i) => i.url === url);
|
|
||||||
if (item) item.tematik = tematik;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSvodkaChange = ({ url, svodka }) => {
|
|
||||||
const item = items.value.find((i) => i.url === url);
|
|
||||||
if (item) item.svodka = svodka;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDonesenieChange = ({ url, donesenie }) => {
|
|
||||||
const item = items.value.find((i) => i.url === url);
|
|
||||||
if (item) item.donesenie = donesenie;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBiluteneChange = ({ url, bilutene }) => {
|
|
||||||
const item = items.value.find((i) => i.url === url);
|
|
||||||
if (item) item.bilutene = bilutene;
|
|
||||||
};
|
|
||||||
|
|
||||||
// === Watch ===
|
|
||||||
|
|
||||||
let debounceTimer = null;
|
|
||||||
watch(poisk, (newVal) => {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
currentOffset = 0;
|
currentOffset = 0
|
||||||
loadItems(currentFilter);
|
loadItems(currentFilter)
|
||||||
}, 800);
|
}, 800)
|
||||||
});
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.filter,
|
() => props.filter,
|
||||||
(newFilter) => {
|
(newFilter) => {
|
||||||
const filterValue = newFilter === "all" ? "default" : newFilter;
|
const filterValue = newFilter === 'all' ? 'default' : newFilter
|
||||||
currentFilter = filterValue;
|
activeFilter.value = newFilter === 'all' ? 'all' : newFilter
|
||||||
poisk.value = "";
|
currentFilter = filterValue
|
||||||
loadItems(filterValue);
|
searchQuery.value = ''
|
||||||
|
loadItems(filterValue)
|
||||||
},
|
},
|
||||||
);
|
{ immediate: true },
|
||||||
|
)
|
||||||
const onfilterItems = (filterValue) => {
|
|
||||||
const filter = filterValue === "all" ? "default" : filterValue;
|
|
||||||
loadItems(filter);
|
|
||||||
};
|
|
||||||
|
|
||||||
// === Lifecycle ===
|
|
||||||
|
|
||||||
let observer = null;
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Тема
|
startPolling()
|
||||||
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(() => {
|
nextTick(() => {
|
||||||
observer = new IntersectionObserver(
|
observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting && hasMore.value) {
|
if (entries[0].isIntersecting && hasMore.value) {
|
||||||
loadItems(currentFilter, true); // append = true
|
loadItems(currentFilter, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: "100px" },
|
{ rootMargin: '100px' },
|
||||||
);
|
)
|
||||||
|
|
||||||
if (sentinel.value) {
|
if (sentinel.value) {
|
||||||
observer.observe(sentinel.value);
|
observer.observe(sentinel.value)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPolling();
|
stopPolling()
|
||||||
if (observer) observer.disconnect();
|
if (observer) observer.disconnect()
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
269
src/components/News_section/NewsCard.vue
Normal file
269
src/components/News_section/NewsCard.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
||||||
|
import { useDarkMode } from "@/composables/useDarkMode.js";
|
||||||
|
import { updateNewsStatus } from "@/services/newsService.js";
|
||||||
|
import { api8001 } from "@/services/api.js";
|
||||||
|
import { downloadBlob, extractFilenameFromHeaders } from "@/utils/download.js";
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"update:viewed",
|
||||||
|
"update:status",
|
||||||
|
"update:tematik",
|
||||||
|
"update:svodka",
|
||||||
|
"update:donesenie",
|
||||||
|
"update:bilutene",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
url: { type: String, required: true },
|
||||||
|
parsed_at: String,
|
||||||
|
title: { type: String, required: true },
|
||||||
|
category: String,
|
||||||
|
short_text: String,
|
||||||
|
article_date: String,
|
||||||
|
original_text: String,
|
||||||
|
translation_text: String,
|
||||||
|
other: String,
|
||||||
|
viewed: Boolean,
|
||||||
|
status: Boolean,
|
||||||
|
tematik: Boolean,
|
||||||
|
svodka: Boolean,
|
||||||
|
donesenie: Boolean,
|
||||||
|
bilutene: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
|
||||||
|
const flags = ref({
|
||||||
|
viewed: props.viewed,
|
||||||
|
status: props.status,
|
||||||
|
tematik: props.tematik,
|
||||||
|
svodka: props.svodka,
|
||||||
|
donesenie: props.donesenie,
|
||||||
|
bilutene: props.bilutene,
|
||||||
|
});
|
||||||
|
|
||||||
|
const flagFields = ['viewed', 'status', 'tematik', 'svodka', 'donesenie', 'bilutene'];
|
||||||
|
flagFields.forEach((field) => {
|
||||||
|
watch(() => props[field], (val) => {
|
||||||
|
flags.value[field] = val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const cardRef = ref(null);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (cardRef.value && !cardRef.value.contains(event.target)) {
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleFlag(field) {
|
||||||
|
const newValue = !flags.value[field];
|
||||||
|
flags.value[field] = newValue;
|
||||||
|
emit(`update:${field}`, { url: props.url, [field]: newValue });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateNewsStatus(props.url, field, newValue);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
flags.value[field] = !newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
try {
|
||||||
|
const { data, headers } = await api8001.get("/file_download", {
|
||||||
|
params: {
|
||||||
|
path: props.article_date?.split(" ")[0],
|
||||||
|
title: props.title,
|
||||||
|
},
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallback = `${props.title}.docx`;
|
||||||
|
const filename = extractFilenameFromHeaders(headers, fallback);
|
||||||
|
downloadBlob(data, filename);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка скачивания:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-gray-800 hover:-translate-y-2 hover:shadow-2xl rounded-xl transition pb-1 mb-3"
|
||||||
|
:class="{ 'opacity-50': flags.viewed }"
|
||||||
|
>
|
||||||
|
<div ref="cardRef" class="cursor-pointer" @click="toggle">
|
||||||
|
<div class="flex justify-between p-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p>{{ article_date }}</p>
|
||||||
|
<span class="text-sm">
|
||||||
|
<img
|
||||||
|
v-if="other === 'source1' || other === 'source2'"
|
||||||
|
src="https://img.icons8.com/?size=100&id=OafC2pWK4RV4&format=png&color=000000"
|
||||||
|
class="h-6 w-auto"
|
||||||
|
alt="Китай"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="other === 'Япония'"
|
||||||
|
src="https://img.icons8.com/?size=100&id=KvglG3FkCenH&format=png&color=000000"
|
||||||
|
class="h-6 w-auto"
|
||||||
|
alt="Япония"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="other === 'Корея'"
|
||||||
|
src="https://img.icons8.com/?size=100&id=uCynf758t5TG&format=png&color=000000"
|
||||||
|
class="h-6 w-auto"
|
||||||
|
alt="Корея"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="max-w-50 xl:max-w-100 text-blue-600 dark:text-gray-400 hover:text-blue-800 active:text-blue-800 underline flex truncate"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<img src="/src/assets/href.webp" class="w-5 h-6 mr-2 shrink-0" alt="" />
|
||||||
|
<span class="truncate">{{ url }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
||||||
|
<p class="font-medium">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="m-3 bg-gray-300 dark:bg-gray-950 shadow rounded-xl p-3 max-w-170">
|
||||||
|
<p>Категория: {{ category }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isOpen" class="transition-all">
|
||||||
|
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Краткое содержание</p>
|
||||||
|
<p>{{ short_text }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Перевод</p>
|
||||||
|
<p>{{ translation_text }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Оригинал</p>
|
||||||
|
<p>{{ original_text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="ml-4 mb-4 flex items-end gap-2">
|
||||||
|
<button type="button" class="cursor-pointer" @click.stop="toggleFlag('viewed')">
|
||||||
|
<img
|
||||||
|
v-if="flags.viewed"
|
||||||
|
src="https://img.icons8.com/?size=100&id=IJNt9jGwqy9N&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="Просмотрено"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isDark"
|
||||||
|
src="https://img.icons8.com/?size=100&id=qliQ29ghmkZh&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="Отметить просмотренным"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="https://img.icons8.com/?size=100&id=KDfrR4UXlVPn&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="Отметить просмотренным"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="cursor-pointer" @click.stop="toggleFlag('status')">
|
||||||
|
<img
|
||||||
|
v-if="flags.status"
|
||||||
|
src="https://img.icons8.com/?size=100&id=fiJBCEhvIMyT&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="В избранном"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isDark"
|
||||||
|
src="https://img.icons8.com/?size=100&id=BmD1kkH92ppy&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="Добавить в избранное"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="https://img.icons8.com/?size=100&id=wVQaONwgqX8h&format=png&color=000000"
|
||||||
|
class="h-12 hover:opacity-75 active:opacity-75"
|
||||||
|
alt="Добавить в избранное"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click.stop="toggleFlag('tematik')"
|
||||||
|
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
||||||
|
:class="flags.tematik
|
||||||
|
? 'bg-gradient-to-r from-red-700 to-red-400 text-white'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
|
||||||
|
>
|
||||||
|
Т
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click.stop="toggleFlag('svodka')"
|
||||||
|
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
||||||
|
:class="flags.svodka
|
||||||
|
? 'bg-gradient-to-r from-blue-900 to-blue-500 text-white'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
|
||||||
|
>
|
||||||
|
С
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click.stop="toggleFlag('donesenie')"
|
||||||
|
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
||||||
|
:class="flags.donesenie
|
||||||
|
? 'bg-gradient-to-r from-green-700 to-green-400 text-white'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
|
||||||
|
>
|
||||||
|
Д
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click.stop="toggleFlag('bilutene')"
|
||||||
|
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
||||||
|
:class="flags.bilutene
|
||||||
|
? 'bg-gradient-to-r from-olive-700 to-olive-500 text-white'
|
||||||
|
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
|
||||||
|
>
|
||||||
|
Б
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hover:opacity-75 active:opacity-75 mx-5 rounded-xl cursor-pointer"
|
||||||
|
@click.stop="download"
|
||||||
|
>
|
||||||
|
<img src="/src/assets/word.png" class="h-10 mb-2" alt="Скачать DOCX" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
// Добавляем emit для уведомления родителя об изменениях
|
|
||||||
const emit = defineEmits([
|
|
||||||
"update:viewed",
|
|
||||||
"update:status",
|
|
||||||
"update:tematik",
|
|
||||||
"update:svodka",
|
|
||||||
"update:donesenie",
|
|
||||||
"update:bilutene",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
url: String,
|
|
||||||
parsed_at: String,
|
|
||||||
title: String,
|
|
||||||
category: String,
|
|
||||||
short_text: String,
|
|
||||||
article_date: String,
|
|
||||||
original_text: String,
|
|
||||||
translation_text: String,
|
|
||||||
other: String,
|
|
||||||
viewed: Boolean,
|
|
||||||
status: Boolean,
|
|
||||||
tematik: Boolean,
|
|
||||||
svodka: Boolean,
|
|
||||||
donesenie: Boolean,
|
|
||||||
bilutene: Boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Делаем viewed и status реактивными для локального обновления
|
|
||||||
const localViewed = ref(props.viewed);
|
|
||||||
const localStatus = ref(props.status);
|
|
||||||
|
|
||||||
// Делаем tematik, svodka, donesenie, bilutene реактивными для локального обновления
|
|
||||||
const localTematik = ref(props.tematik);
|
|
||||||
const localSvodka = ref(props.svodka);
|
|
||||||
const localDonesenie = ref(props.donesenie);
|
|
||||||
const localBilutene = ref(props.bilutene);
|
|
||||||
|
|
||||||
// Следим за изменениями props и обновляем локальные значения
|
|
||||||
watch(
|
|
||||||
() => props.viewed,
|
|
||||||
(val) => {
|
|
||||||
localViewed.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.status,
|
|
||||||
(val) => {
|
|
||||||
localStatus.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.tematik,
|
|
||||||
(val) => {
|
|
||||||
localTematik.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.svodka,
|
|
||||||
(val) => {
|
|
||||||
localSvodka.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.donesenie,
|
|
||||||
(val) => {
|
|
||||||
localDonesenie.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.bilutene,
|
|
||||||
(val) => {
|
|
||||||
localBilutene.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Тема
|
|
||||||
const isDarkMode = ref(document.documentElement.classList.contains("dark"));
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
isDarkMode.value = document.documentElement.classList.contains("dark");
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
isDarkMode.value = document.documentElement.classList.contains("dark");
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Карточка
|
|
||||||
const isOpen = ref(false);
|
|
||||||
const cardRef = ref(null);
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
isOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClickOutside(event) {
|
|
||||||
if (cardRef.value && !cardRef.value.contains(event.target)) {
|
|
||||||
isOpen.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем локально + отправляем emit
|
|
||||||
const viewed_b = async () => {
|
|
||||||
const newValue = !localViewed.value;
|
|
||||||
localViewed.value = newValue; // Локальное обновление сразу
|
|
||||||
emit("update:viewed", { url: props.url, viewed: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_viewed_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, viewed: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
// При ошибке откатываем локальное значение
|
|
||||||
localViewed.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const status_b = async () => {
|
|
||||||
const newValue = !localStatus.value;
|
|
||||||
localStatus.value = newValue; // Локальное обновление сразу
|
|
||||||
emit("update:status", { url: props.url, status: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_status_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, status: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
localStatus.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tematik_b = async () => {
|
|
||||||
const newValue = !localTematik.value;
|
|
||||||
localTematik.value = newValue;
|
|
||||||
emit("update:tematik", { url: props.url, tematik: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_tematik_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, tematik: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
localTematik.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const svodka_b = async () => {
|
|
||||||
const newValue = !localSvodka.value;
|
|
||||||
localSvodka.value = newValue;
|
|
||||||
emit("update:svodka", { url: props.url, svodka: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_svodka_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, svodka: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
localSvodka.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const donesenie_b = async () => {
|
|
||||||
const newValue = !localDonesenie.value;
|
|
||||||
localDonesenie.value = newValue;
|
|
||||||
emit("update:donesenie", { url: props.url, donesenie: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_donesenie_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, donesenie: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
localDonesenie.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const bilutene_b = async () => {
|
|
||||||
const newValue = !localBilutene.value;
|
|
||||||
localBilutene.value = newValue;
|
|
||||||
emit("update:bilutene", { url: props.url, bilutene: newValue });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8002/update_bilutene_status",
|
|
||||||
null,
|
|
||||||
{ params: { url: props.url, bilutene: newValue } },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
localBilutene.value = !newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const download = async () => {
|
|
||||||
try {
|
|
||||||
// Обрезаем заголовок до 200 символов для совместимости с именем файла на сервере
|
|
||||||
// const truncatedTitle = props.title.slice(0, 100);
|
|
||||||
const rez = await axios.get(
|
|
||||||
"https://allowlgroup.ru/api/8001/file_download",
|
|
||||||
// "http://127.0.0.1:8001/file_download",
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
// path: props.parsed_at.split(" ")[0].replace("-", "/"),
|
|
||||||
path: props.article_date.split(" ")[0],
|
|
||||||
title: props.title,
|
|
||||||
},
|
|
||||||
responseType: "blob",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const blobUrl = window.URL.createObjectURL(
|
|
||||||
new Blob([rez.data], {
|
|
||||||
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
let filename = props.title + ".docx";
|
|
||||||
try {
|
|
||||||
const disposition = rez.headers?.["content-disposition"];
|
|
||||||
if (disposition) {
|
|
||||||
const match = disposition.match(/filename\*?=([^;\n]+)/i);
|
|
||||||
if (match) {
|
|
||||||
let name = match[1];
|
|
||||||
// Убираем префикс utf-8'' (case insensitive)
|
|
||||||
const prefix = "utf-8''";
|
|
||||||
if (name.toLowerCase().startsWith(prefix)) {
|
|
||||||
name = name.substring(prefix.length);
|
|
||||||
}
|
|
||||||
// Убираем кавычки и декодируем
|
|
||||||
name = decodeURIComponent(name.replace(/"/g, ""));
|
|
||||||
if (!name.endsWith(".docx")) name += ".docx";
|
|
||||||
filename = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
link.href = blobUrl;
|
|
||||||
link.setAttribute("download", filename);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
window.URL.revokeObjectURL(blobUrl);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="bg-white dark:bg-gray-800 hover:-translate-y-2 hover:shadow-2xl rounded-xl transition pb-1 mb-3"
|
|
||||||
:class="{ 'opacity-50': localViewed }"
|
|
||||||
>
|
|
||||||
<div ref="cardRef" class="cursor-pointer" @click="toggle">
|
|
||||||
<div class="flex justify-between p-3">
|
|
||||||
<div class="flex">
|
|
||||||
<p>{{ article_date }}</p>
|
|
||||||
<span class="text-sm ml-2">
|
|
||||||
<img
|
|
||||||
v-if="other === 'source1' || other === 'source2'"
|
|
||||||
src="https://img.icons8.com/?size=100&id=OafC2pWK4RV4&format=png&color=000000"
|
|
||||||
class="h-6 w-auto"
|
|
||||||
alt="Китай"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="other === 'Япония'"
|
|
||||||
src="https://img.icons8.com/?size=100&id=KvglG3FkCenH&format=png&color=000000"
|
|
||||||
class="h-6 w-auto"
|
|
||||||
alt="Япония"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="other === 'Корея'"
|
|
||||||
src="https://img.icons8.com/?size=100&id=uCynf758t5TG&format=png&color=000000"
|
|
||||||
class="h-6 w-auto"
|
|
||||||
alt="Корея"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:href="url"
|
|
||||||
class="max-w-50 xl:max-w-100 text-blue-600 dark:text-gray-400 hover:text-blue-800 active:text-blue-800 underline flex"
|
|
||||||
style="overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
|
||||||
>
|
|
||||||
<img src="/src/assets/href.webp" class="w-5 h-6 mr-2" />
|
|
||||||
{{ url }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
|
||||||
<p>{{ title }}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="m-3 bg-gray-300 dark:bg-gray-950 shadow rounded-xl p-3 max-w-170"
|
|
||||||
>
|
|
||||||
<p>Категория: {{ category }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="isOpen" class="transition-all">
|
|
||||||
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
|
||||||
<p>{{ short_text }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
|
||||||
<p>{{ translation_text }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
|
|
||||||
<p>{{ original_text }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="ml-4 mb-4 flex items-end gap-2">
|
|
||||||
<button class="cursor-pointer" @click="viewed_b">
|
|
||||||
<img
|
|
||||||
v-if="localViewed"
|
|
||||||
src="https://img.icons8.com/?size=100&id=IJNt9jGwqy9N&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="isDarkMode"
|
|
||||||
src="https://img.icons8.com/?size=100&id=qliQ29ghmkZh&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
src="https://img.icons8.com/?size=100&id=KDfrR4UXlVPn&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="cursor-pointer" @click="status_b">
|
|
||||||
<img
|
|
||||||
v-if="localStatus"
|
|
||||||
src="https://img.icons8.com/?size=100&id=fiJBCEhvIMyT&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="isDarkMode"
|
|
||||||
src="https://img.icons8.com/?size=100&id=BmD1kkH92ppy&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
src="https://img.icons8.com/?size=100&id=wVQaONwgqX8h&format=png&color=000000"
|
|
||||||
class="h-12 hover:opacity-75 active:opacity-75"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Кнопки Т, С, Д, Б -->
|
|
||||||
<button
|
|
||||||
@click="tematik_b"
|
|
||||||
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
|
||||||
:class="
|
|
||||||
localTematik
|
|
||||||
? 'bg-gradient-to-r from-red-700 to-red-400 text-white'
|
|
||||||
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Т
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="svodka_b"
|
|
||||||
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
|
||||||
:class="
|
|
||||||
localSvodka
|
|
||||||
? 'bg-gradient-to-r from-blue-900 to-blue-500 text-white'
|
|
||||||
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
С
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="donesenie_b"
|
|
||||||
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
|
||||||
:class="
|
|
||||||
localDonesenie
|
|
||||||
? 'bg-gradient-to-r from-green-700 to-green-400 text-white'
|
|
||||||
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Д
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="bilutene_b"
|
|
||||||
class="w-10 h-10 rounded-lg font-bold text-sm hover:opacity-75 active:opacity-75 transition-colors cursor-pointer"
|
|
||||||
:class="
|
|
||||||
localBilutene
|
|
||||||
? 'bg-gradient-to-r from-olive-700 to-olive-500 text-white'
|
|
||||||
: 'bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Б
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="hover:opacity-75 active:opacity-75 mx-5 rounded-xl cursor-pointer"
|
|
||||||
@click="download"
|
|
||||||
>
|
|
||||||
<img src="/src/assets/word.png" class="h-10 mb-2" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -4,42 +4,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
data() {
|
import { formatUtcDateTime } from '@/utils/date.js'
|
||||||
return {
|
|
||||||
currentDateTime: "", // строка с текущими датой и временем
|
|
||||||
timer: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateTime() {
|
|
||||||
const options = {
|
|
||||||
timeZone: "UTC",
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
};
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-GB", options);
|
|
||||||
const parts = formatter.formatToParts(new Date());
|
|
||||||
|
|
||||||
const dateParts = {};
|
const currentDateTime = ref('')
|
||||||
parts.forEach(({ type, value }) => {
|
let timer = null
|
||||||
dateParts[type] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentDateTime = `${dateParts.year}-${dateParts.month}-${dateParts.day} ${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
function updateTime() {
|
||||||
},
|
currentDateTime.value = formatUtcDateTime()
|
||||||
},
|
}
|
||||||
mounted() {
|
|
||||||
this.updateTime();
|
onMounted(() => {
|
||||||
this.timer = setInterval(this.updateTime, 1000);
|
updateTime()
|
||||||
},
|
timer = setInterval(updateTime, 1000)
|
||||||
beforeDestroy() {
|
})
|
||||||
clearInterval(this.timer);
|
|
||||||
},
|
onBeforeUnmount(() => {
|
||||||
};
|
clearInterval(timer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="date-input-container relative flex-grow">
|
<div class="date-input-container relative flex-grow">
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
v-model="model"
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
|
||||||
type="date"
|
type="date"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="date-input dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 cursor-pointer w-full appearance-none"
|
class="date-input dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 cursor-pointer w-full appearance-none"
|
||||||
@@ -27,18 +26,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
const model = defineModel({ type: String, default: '' })
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
defineEmits(["update:modelValue"]);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-full sm:w-4/5 dark:text-neutral-300">
|
|
||||||
<!-- Ввод времени -->
|
|
||||||
<div class="bg-white p-4 mb-4 flex-colum sm:flex dark:bg-gray-800">
|
|
||||||
<div class="w-full sm:max-w-100 flex">
|
|
||||||
<DatePicker v-model="time" placeholder="01.01.2026" />
|
|
||||||
<button
|
|
||||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 ml-4 shadow text-white bg-sky-700 w-1/2 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
|
||||||
@click="start_parser_1"
|
|
||||||
>
|
|
||||||
Парсить 1 источник
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 sm:ml-2 shadow mt-1 sm:mt-0 text-white w-full sm:max-w-50 bg-sky-700 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
|
||||||
@click="start_parser_2"
|
|
||||||
>
|
|
||||||
Парсить 2 источник
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Список источников и промт -->
|
|
||||||
<div
|
|
||||||
class="dark:bg-gray-800 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex-colum sm:flex items-center mb-4 border-slate-900 justify-between"
|
|
||||||
>
|
|
||||||
<div class="w-full sm:max-w-60 flex">
|
|
||||||
<button
|
|
||||||
v-for="(source, index) in sources"
|
|
||||||
:key="source.name"
|
|
||||||
@click="selectSource(index)"
|
|
||||||
class="dark:bg-neutral-500 hover:dark:bg-neutral-600 p-2 mr-2 rounded-xl bg-sky-700 hover:bg-sky-900 w-1/2 text-white cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ source.name }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Кнопка сохранения источников -->
|
|
||||||
<button
|
|
||||||
class="shadow mt-1 sm:mt-0 text-white bg-green-500 hover:bg-green-600 rounded-xl w-full sm:max-w-60 px-2 min-h-11 cursor-pointer"
|
|
||||||
@click="saveSources"
|
|
||||||
>
|
|
||||||
Сохранить изменения
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Редактируемый промт -->
|
|
||||||
<div class=" ">
|
|
||||||
<textarea
|
|
||||||
v-model="promt"
|
|
||||||
class="w-full min-h-100 rounded-xl p-2 border-2 border-neutral-300"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Список логов -->
|
|
||||||
<div
|
|
||||||
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
|
||||||
>
|
|
||||||
<!-- Кнопка для переключения -->
|
|
||||||
<button
|
|
||||||
@click="toggleLogs"
|
|
||||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ showLogs ? "Скрыть логи" : "Показать логи" }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Блок логов, показывается или скрывается -->
|
|
||||||
<div
|
|
||||||
class="border-1 border-neutral-300 p-2 mt-2 rounded-xl"
|
|
||||||
v-if="showLogs"
|
|
||||||
>
|
|
||||||
<div v-for="(log, index) in logs" :key="index">
|
|
||||||
{{ log }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
|
||||||
>
|
|
||||||
<!-- Кнопка для переключения -->
|
|
||||||
<button
|
|
||||||
@click="toggletask"
|
|
||||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ showtask ? "Скрыть задачи" : "Показать задачи" }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
class="border-1 border-neutral-300 p-2 mt-2 rounded-xl overflow-x-auto"
|
|
||||||
v-if="showtask"
|
|
||||||
>
|
|
||||||
<table class="w-full min-w-max">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="px-2 py-2 text-left">Status</th>
|
|
||||||
<th class="px-2 py-2 text-left">Source URL</th>
|
|
||||||
<th class="px-2 py-2 text-left">Time start</th>
|
|
||||||
<th class="px-2 py-2 text-left">Time finish</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="task in tasks" :key="task.id">
|
|
||||||
<td
|
|
||||||
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
|
|
||||||
>
|
|
||||||
{{ task.status }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
|
|
||||||
>
|
|
||||||
{{ task.source_url }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
|
|
||||||
>
|
|
||||||
{{ task.created_at }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
|
|
||||||
>
|
|
||||||
{{ task.finished_at }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
class="text-red-500 p-1 rounded-full hover:text-red-700 cursor-pointer"
|
|
||||||
@click="delete_row(task.id)"
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
|
||||||
>
|
|
||||||
<Setings_downloads />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
import Setings_downloads from "./Setings_downloads.vue";
|
|
||||||
import DatePicker from "./DatePicker.vue";
|
|
||||||
|
|
||||||
const sources = ref([{ name: "", promt: "" }]);
|
|
||||||
const time = ref(getTodayDate());
|
|
||||||
const currentSource = ref(sources.value[0]);
|
|
||||||
const promt = ref("Начальный текст");
|
|
||||||
const logs = ref([]);
|
|
||||||
const tasks = ref([]);
|
|
||||||
const showLogs = ref(false); // скрыто по умолчанию
|
|
||||||
const showtask = ref(false); // скрыто по умолчанию
|
|
||||||
|
|
||||||
// Функция для получения сегодняшней даты в формате YYYY-MM-DD
|
|
||||||
function getTodayDate() {
|
|
||||||
return new Date().toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// функция для переключения видимости логов
|
|
||||||
function toggleLogs() {
|
|
||||||
showLogs.value = !showLogs.value;
|
|
||||||
if (showLogs.value) {
|
|
||||||
load_log();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const load_log = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get("https://allowlgroup.ru/api/8001/logs");
|
|
||||||
logs.value = response.data.logs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при загрузке настроек:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// функция для переключения видимости задач
|
|
||||||
function toggletask() {
|
|
||||||
showtask.value = !showtask.value;
|
|
||||||
if (showtask.value) {
|
|
||||||
loadTasks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadTasks = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
"https://allowlgroup.ru/api/8001/get_tasks_offset",
|
|
||||||
);
|
|
||||||
tasks.value = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при загрузке задач:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const delete_row = async (id) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.delete(
|
|
||||||
`https://allowlgroup.ru/api/8001/delete_task/${id}`,
|
|
||||||
);
|
|
||||||
loadTasks();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при удалении задачи:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загрузка настроек при монтировании
|
|
||||||
onMounted(() => {
|
|
||||||
loadSettings();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение настроек с сервера
|
|
||||||
const loadSettings = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
"https://allowlgroup.ru/api/8001/settings",
|
|
||||||
);
|
|
||||||
sources.value = response.data.sources;
|
|
||||||
currentSource.value = sources.value[0];
|
|
||||||
promt.value = currentSource.value.promt;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при загрузке настроек:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Выбор источника
|
|
||||||
const selectSource = (index) => {
|
|
||||||
currentSource.value = sources.value[index];
|
|
||||||
promt.value = currentSource.value.promt;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Сохранение источников
|
|
||||||
const saveSources = async () => {
|
|
||||||
try {
|
|
||||||
await axios.post("https://allowlgroup.ru/api/8001/settings", {
|
|
||||||
name: currentSource.value.name,
|
|
||||||
promt: promt.value,
|
|
||||||
});
|
|
||||||
// Здесь можно отправить на сервер, если нужно
|
|
||||||
loadSettings();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при сохранении источников:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запуск парсинга 1
|
|
||||||
const start_parser_1 = async () => {
|
|
||||||
try {
|
|
||||||
// await axios.post("http://127.0.0.1:8001/parser_1", {
|
|
||||||
await axios.post("https://allowlgroup.ru/api/8001/parser_1", {
|
|
||||||
time: time.value,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка запроса parser_1:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запуск парсинга 2
|
|
||||||
const start_parser_2 = async () => {
|
|
||||||
try {
|
|
||||||
await axios.post("https://allowlgroup.ru/api/8001/parser_2");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка запроса parser_2:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-wrap items-center gap-2 w-full justify-between">
|
|
||||||
<!-- Поля ввода дат в одной строке -->
|
|
||||||
<div class="flex flex-col lg:flex-row items-center gap-2 flex-grow">
|
|
||||||
<div class="flex items-center gap-2 w-full lg:w-auto">
|
|
||||||
<span class="dark:text-neutral-300 whitespace-nowrap">с</span>
|
|
||||||
<DatePicker v-model="data_start" />
|
|
||||||
<span class="dark:text-neutral-300 whitespace-nowrap">по</span>
|
|
||||||
<DatePicker v-model="data_finish" />
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
v-model="selectedFilter"
|
|
||||||
@change="onfilterItems($event.target.value)"
|
|
||||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3 mt-3 lg:mt-0 lg:ml-4 w-full lg:w-auto flex-shrink-0 cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="status">Избранные</option>
|
|
||||||
<option value="tematik">Тематическая</option>
|
|
||||||
<option value="svodka">Сводка</option>
|
|
||||||
<option value="donesenie">Донесение</option>
|
|
||||||
<option value="bilutene">Билутень</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Кнопка выгрузки -->
|
|
||||||
<button
|
|
||||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer w-full lg:w-auto md:min-w-40 flex-shrink-0"
|
|
||||||
@click="downloadAll"
|
|
||||||
>
|
|
||||||
Выгрузить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
import DatePicker from "./DatePicker.vue";
|
|
||||||
|
|
||||||
// Переменные для выгрузки с датами по умолчанию
|
|
||||||
const data_start = ref(getYesterdayDate());
|
|
||||||
const data_finish = ref(getTodayDate());
|
|
||||||
const selectedFilter = ref("status"); // По умолчанию "Избранные"
|
|
||||||
|
|
||||||
// Функция для получения вчерашней даты в формате YYYY-MM-DD
|
|
||||||
function getYesterdayDate() {
|
|
||||||
const date = new Date();
|
|
||||||
date.setDate(date.getDate() - 1);
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для получения сегодняшней даты в формате YYYY-MM-DD
|
|
||||||
function getTodayDate() {
|
|
||||||
return new Date().toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик выбора фильтра
|
|
||||||
const onfilterItems = (value) => {
|
|
||||||
selectedFilter.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Выгрузка
|
|
||||||
const downloadAll = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
|
||||||
"https://allowlgroup.ru/api/8001/download_all",
|
|
||||||
// "http://127.0.0.1:8001/download_all",
|
|
||||||
{
|
|
||||||
data_start: data_start.value,
|
|
||||||
data_finish: data_finish.value,
|
|
||||||
field_name: selectedFilter.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
responseType: "blob",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute(
|
|
||||||
"download",
|
|
||||||
`${selectedFilter.value}_${data_start.value}_${data_finish.value}.zip`,
|
|
||||||
);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка при выгрузке:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
248
src/components/Settings_section/Settings.vue
Normal file
248
src/components/Settings_section/Settings.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full sm:w-4/5 dark:text-neutral-300">
|
||||||
|
<!-- Ввод времени -->
|
||||||
|
<div class="bg-white p-4 mb-4 flex-colum sm:flex dark:bg-gray-800">
|
||||||
|
<div class="w-full sm:max-w-100 flex">
|
||||||
|
<DatePicker v-model="time" placeholder="01.01.2026" />
|
||||||
|
<button
|
||||||
|
class="dark:bg-orange-500 hover:dark:bg-orange-600 ml-4 shadow text-white bg-sky-700 w-1/2 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
||||||
|
@click="start_parser_1"
|
||||||
|
>
|
||||||
|
Парсить 1 источник
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="dark:bg-orange-500 hover:dark:bg-orange-600 sm:ml-2 shadow mt-1 sm:mt-0 text-white w-full sm:max-w-50 bg-sky-700 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
||||||
|
@click="start_parser_2"
|
||||||
|
>
|
||||||
|
Парсить 2 источник
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Список источников и промт -->
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-800 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex-colum md:flex items-center mb-4 justify-between"
|
||||||
|
>
|
||||||
|
<div class="w-full md:max-w-60 flex">
|
||||||
|
<button
|
||||||
|
v-for="(source, index) in sources"
|
||||||
|
:key="source.name"
|
||||||
|
type="button"
|
||||||
|
@click="selectSource(index)"
|
||||||
|
:class="[
|
||||||
|
'p-2 mr-2 rounded-xl w-1/2 text-white cursor-pointer transition-colors',
|
||||||
|
currentSource?.name === source.name
|
||||||
|
? 'bg-sky-900 dark:bg-neutral-700 ring-2 ring-offset-1 ring-sky-300 dark:ring-neutral-400'
|
||||||
|
: 'bg-sky-700 hover:bg-sky-900 dark:bg-neutral-500 dark:hover:bg-neutral-600',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ source.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shadow mt-1 md:mt-0 text-white bg-green-500 hover:bg-green-600 rounded-xl w-full md:max-w-60 px-2 min-h-11 cursor-pointer"
|
||||||
|
@click="saveSources"
|
||||||
|
>
|
||||||
|
Сохранить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-model="prompt"
|
||||||
|
class="w-full min-h-96 rounded-xl p-2 border-2 border-neutral-300 dark:bg-gray-900 dark:border-neutral-600"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Список логов -->
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="toggleLogs"
|
||||||
|
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ showLogs ? 'Скрыть логи' : 'Показать логи' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="showLogs" class="border border-neutral-300 p-2 mt-2 rounded-xl">
|
||||||
|
<div v-for="(log, index) in logs" :key="index" class="font-mono text-sm">
|
||||||
|
{{ log }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Список задач -->
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="toggleTask"
|
||||||
|
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ showTask ? 'Скрыть задачи' : 'Показать задачи' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="showTask" class="border border-neutral-300 p-2 mt-2 rounded-xl overflow-x-auto">
|
||||||
|
<table class="w-full min-w-max">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-neutral-200 dark:border-neutral-600">
|
||||||
|
<th class="px-2 py-2 text-left">Status</th>
|
||||||
|
<th class="px-2 py-2 text-left">Source URL</th>
|
||||||
|
<th class="px-2 py-2 text-left">Time start</th>
|
||||||
|
<th class="px-2 py-2 text-left">Time finish</th>
|
||||||
|
<th class="px-2 py-2 text-left">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="task in tasks" :key="task.id" class="border-b border-neutral-100 dark:border-neutral-700 last:border-0">
|
||||||
|
<td class="px-2 py-2 whitespace-nowrap">{{ task.status }}</td>
|
||||||
|
<td class="px-2 py-2 whitespace-nowrap">{{ task.source_url }}</td>
|
||||||
|
<td class="px-2 py-2 whitespace-nowrap">{{ task.created_at }}</td>
|
||||||
|
<td class="px-2 py-2 whitespace-nowrap">{{ task.finished_at }}</td>
|
||||||
|
<td class="px-2 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-red-500 p-1 rounded-full hover:text-red-700 cursor-pointer"
|
||||||
|
@click="deleteTask(task.id)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<SettingsDownloads />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
fetchSettings,
|
||||||
|
saveSettings as apiSaveSettings,
|
||||||
|
fetchLogs,
|
||||||
|
fetchTasks,
|
||||||
|
deleteTask as apiDeleteTask,
|
||||||
|
startParser1 as apiStartParser1,
|
||||||
|
startParser2 as apiStartParser2,
|
||||||
|
} from '@/services/settingsService.js'
|
||||||
|
import { getTodayDate } from '@/utils/date.js'
|
||||||
|
import SettingsDownloads from './SettingsDownloads.vue'
|
||||||
|
import DatePicker from './DatePicker.vue'
|
||||||
|
|
||||||
|
const sources = ref([{ name: '', promt: '' }])
|
||||||
|
const time = ref(getTodayDate())
|
||||||
|
const currentSource = ref(null)
|
||||||
|
const prompt = ref('')
|
||||||
|
const logs = ref([])
|
||||||
|
const tasks = ref([])
|
||||||
|
const showLogs = ref(false)
|
||||||
|
const showTask = ref(false)
|
||||||
|
|
||||||
|
let isSyncing = false
|
||||||
|
|
||||||
|
// Синхронизируем prompt обратно в currentSource
|
||||||
|
watch(prompt, (val) => {
|
||||||
|
if (currentSource.value && !isSyncing) {
|
||||||
|
currentSource.value.promt = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleLogs() {
|
||||||
|
showLogs.value = !showLogs.value
|
||||||
|
if (showLogs.value) loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
try {
|
||||||
|
logs.value = await fetchLogs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке логов:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTask() {
|
||||||
|
showTask.value = !showTask.value
|
||||||
|
if (showTask.value) loadTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTasks() {
|
||||||
|
try {
|
||||||
|
tasks.value = await fetchTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке задач:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTask(id) {
|
||||||
|
try {
|
||||||
|
await apiDeleteTask(id)
|
||||||
|
await loadTasks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при удалении задачи:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const data = await fetchSettings()
|
||||||
|
sources.value = data.sources
|
||||||
|
if (sources.value.length) {
|
||||||
|
selectSource(0)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке настроек:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSource(index) {
|
||||||
|
isSyncing = true
|
||||||
|
currentSource.value = sources.value[index]
|
||||||
|
prompt.value = currentSource.value.promt
|
||||||
|
isSyncing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSources() {
|
||||||
|
if (!currentSource.value) return
|
||||||
|
try {
|
||||||
|
await apiSaveSettings(currentSource.value.name, prompt.value)
|
||||||
|
await loadSettings()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении источников:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startParser1() {
|
||||||
|
try {
|
||||||
|
await apiStartParser1(time.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка запроса parser_1:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startParser2() {
|
||||||
|
try {
|
||||||
|
await apiStartParser2()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка запроса parser_2:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
56
src/components/Settings_section/SettingsDownloads.vue
Normal file
56
src/components/Settings_section/SettingsDownloads.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 w-full justify-between">
|
||||||
|
<div class="flex flex-col lg:flex-row items-center gap-2 flex-grow">
|
||||||
|
<div class="flex items-center gap-2 w-full lg:w-auto">
|
||||||
|
<span class="dark:text-neutral-300 whitespace-nowrap">с</span>
|
||||||
|
<DatePicker v-model="dateStart" />
|
||||||
|
<span class="dark:text-neutral-300 whitespace-nowrap">по</span>
|
||||||
|
<DatePicker v-model="dateFinish" />
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
v-model="selectedFilter"
|
||||||
|
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3 mt-3 lg:mt-0 lg:ml-4 w-full lg:w-auto flex-shrink-0 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="status">Избранные</option>
|
||||||
|
<option value="tematik">Тематическая</option>
|
||||||
|
<option value="svodka">Сводка</option>
|
||||||
|
<option value="donesenie">Донесение</option>
|
||||||
|
<option value="bilutene">Билутень</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 hover:bg-sky-900 rounded-xl px-4 min-h-11 cursor-pointer w-full lg:w-auto md:min-w-40 flex-shrink-0"
|
||||||
|
@click="handleDownload"
|
||||||
|
>
|
||||||
|
Выгрузить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { downloadAll } from "@/services/settingsService.js";
|
||||||
|
import { getTodayDate, getYesterdayDate } from "@/utils/date.js";
|
||||||
|
import { downloadBlob } from "@/utils/download.js";
|
||||||
|
import DatePicker from "./DatePicker.vue";
|
||||||
|
|
||||||
|
const dateStart = ref(getYesterdayDate());
|
||||||
|
const dateFinish = ref(getTodayDate());
|
||||||
|
const selectedFilter = ref("status");
|
||||||
|
|
||||||
|
async function handleDownload() {
|
||||||
|
try {
|
||||||
|
const response = await downloadAll(
|
||||||
|
dateStart.value,
|
||||||
|
dateFinish.value,
|
||||||
|
selectedFilter.value,
|
||||||
|
);
|
||||||
|
const filename = `${selectedFilter.value}_${dateStart.value}_${dateFinish.value}.zip`;
|
||||||
|
downloadBlob(response.data, filename);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка при выгрузке:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
25
src/composables/useDarkMode.js
Normal file
25
src/composables/useDarkMode.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
const isDark = ref(false)
|
||||||
|
let observer = null
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
isDark.value = document.documentElement.classList.contains('dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
update()
|
||||||
|
observer = new MutationObserver(update)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (observer) observer.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
return { isDark }
|
||||||
|
}
|
||||||
12
src/main.js
12
src/main.js
@@ -3,4 +3,16 @@ import './assets/main.css'
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
function initTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else if (savedTheme === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initTheme()
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|||||||
20
src/services/api.js
Normal file
20
src/services/api.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_BASE_8001 = 'https://allowlgroup.ru/api/8001'
|
||||||
|
const API_BASE_8002 = 'https://allowlgroup.ru/api/8002'
|
||||||
|
const API_BASE_8004 = 'https://allowlgroup.ru/api/8004'
|
||||||
|
|
||||||
|
export const api8001 = axios.create({
|
||||||
|
baseURL: API_BASE_8001,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const api8002 = axios.create({
|
||||||
|
baseURL: API_BASE_8002,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const api8004 = axios.create({
|
||||||
|
baseURL: API_BASE_8004,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
6
src/services/authService.js
Normal file
6
src/services/authService.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { api8004 } from './api.js'
|
||||||
|
|
||||||
|
export async function login(username, password) {
|
||||||
|
const { data } = await api8004.post('/login', { username, password })
|
||||||
|
return data
|
||||||
|
}
|
||||||
37
src/services/newsService.js
Normal file
37
src/services/newsService.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { api8002 } from './api.js'
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 30
|
||||||
|
|
||||||
|
export async function fetchRecords(filter, offset, limit = DEFAULT_LIMIT) {
|
||||||
|
const { data } = await api8002.get('/records_all', {
|
||||||
|
params: { item: filter, offset, limit },
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRecordsCount(filter) {
|
||||||
|
const { data } = await api8002.get('/records_all/count', {
|
||||||
|
params: { item: filter },
|
||||||
|
})
|
||||||
|
return data.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchRecords(query, filter, offset, limit = DEFAULT_LIMIT) {
|
||||||
|
const { data } = await api8002.get('/poisk', {
|
||||||
|
params: { query, item: filter, offset, limit },
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSearchCount(query, filter) {
|
||||||
|
const { data } = await api8002.get('/poisk/count', {
|
||||||
|
params: { query, item: filter },
|
||||||
|
})
|
||||||
|
return data.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateNewsStatus(url, field, value) {
|
||||||
|
await api8002.post(`/update_${field}_status`, null, {
|
||||||
|
params: { url, [field]: value },
|
||||||
|
})
|
||||||
|
}
|
||||||
40
src/services/settingsService.js
Normal file
40
src/services/settingsService.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { api8001 } from './api.js'
|
||||||
|
|
||||||
|
export async function fetchSettings() {
|
||||||
|
const { data } = await api8001.get('/settings')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveSettings(name, prompt) {
|
||||||
|
await api8001.post('/settings', { name, promt: prompt })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLogs() {
|
||||||
|
const { data } = await api8001.get('/logs')
|
||||||
|
return data.logs ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTasks() {
|
||||||
|
const { data } = await api8001.get('/get_tasks_offset')
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTask(id) {
|
||||||
|
await api8001.delete(`/delete_task/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startParser1(time) {
|
||||||
|
await api8001.post('/parser_1', { time })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startParser2() {
|
||||||
|
await api8001.post('/parser_2')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadAll(dataStart, dataFinish, fieldName) {
|
||||||
|
return api8001.post(
|
||||||
|
'/download_all',
|
||||||
|
{ data_start: dataStart, data_finish: dataFinish, field_name: fieldName },
|
||||||
|
{ responseType: 'blob' },
|
||||||
|
)
|
||||||
|
}
|
||||||
25
src/services/sourceService.js
Normal file
25
src/services/sourceService.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { api8001 } from './api.js'
|
||||||
|
|
||||||
|
export async function fetchCategories() {
|
||||||
|
const { data } = await api8001.get('/categories_promt')
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSources(category = 'all') {
|
||||||
|
const { data } = await api8001.get('/all_sources', {
|
||||||
|
params: { category },
|
||||||
|
})
|
||||||
|
return data.sources ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addSource(url, prompt) {
|
||||||
|
await api8001.post('/add_sources', { url, promt: prompt })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSource(url) {
|
||||||
|
await api8001.delete('/delete_sources', { params: { url } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startSourceParsing(url, prompt) {
|
||||||
|
await api8001.post('/parser_all', { url, promt: prompt })
|
||||||
|
}
|
||||||
31
src/utils/date.js
Normal file
31
src/utils/date.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export function getTodayDate() {
|
||||||
|
return new Date().toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getYesterdayDate() {
|
||||||
|
const date = new Date()
|
||||||
|
date.setDate(date.getDate() - 1)
|
||||||
|
return date.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUtcDateTime() {
|
||||||
|
const options = {
|
||||||
|
timeZone: 'UTC',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-GB', options)
|
||||||
|
const parts = formatter.formatToParts(new Date())
|
||||||
|
const dateParts = {}
|
||||||
|
|
||||||
|
parts.forEach(({ type, value }) => {
|
||||||
|
dateParts[type] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
return `${dateParts.year}-${dateParts.month}-${dateParts.day} ${dateParts.hour}:${dateParts.minute}:${dateParts.second}`
|
||||||
|
}
|
||||||
31
src/utils/download.js
Normal file
31
src/utils/download.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export function downloadBlob(blob, filename) {
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.setAttribute('download', filename)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFilenameFromHeaders(headers, fallback) {
|
||||||
|
try {
|
||||||
|
const disposition = headers?.['content-disposition']
|
||||||
|
if (!disposition) return fallback
|
||||||
|
|
||||||
|
const match = disposition.match(/filename\*?=([^;\n]+)/i)
|
||||||
|
if (!match) return fallback
|
||||||
|
|
||||||
|
let name = match[1]
|
||||||
|
const prefix = "utf-8''"
|
||||||
|
if (name.toLowerCase().startsWith(prefix)) {
|
||||||
|
name = name.substring(prefix.length)
|
||||||
|
}
|
||||||
|
name = decodeURIComponent(name.replace(/"/g, ''))
|
||||||
|
if (!name.endsWith('.docx')) name += '.docx'
|
||||||
|
return name
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user