Files
marketing/src/views/news/NewsDetail.vue
T
2026-02-25 10:33:51 +05:00

328 lines
14 KiB
Vue

<script setup>
import NewsService from '@/service/NewsService';
import Button from 'primevue/button';
import Card from 'primevue/card';
import ProgressSpinner from 'primevue/progressspinner';
import Tag from 'primevue/tag';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
// Реактивные данные
const news = ref(null);
const loading = ref(false);
const error = ref(null);
// Вычисляемые свойства
const hasAnalytics = computed(() => {
return (
news.value?.analytics &&
(news.value.analytics.summary || news.value.analytics.sentiment || (news.value.analytics.tags && news.value.analytics.tags.length > 0) || (news.value.analytics.entities && Object.keys(news.value.analytics.entities).length > 0))
);
});
// Методы
const loadNews = async () => {
const newsId = route.params.id;
if (!newsId) {
error.value = 'ID новости не указан';
return;
}
loading.value = true;
error.value = null;
try {
// Поскольку у нас нет отдельного API для получения одной новости,
// мы будем использовать общий API с фильтром по ID
// В реальном приложении здесь должен быть отдельный эндпоинт
const response = await NewsService.getNewsItems({
page: 0,
size: 1
// Здесь можно добавить фильтр по ID, если API это поддерживает
});
if (response.success && response.data.content.length > 0) {
// Найдем новость по ID (временное решение)
const foundNews = response.data.content.find((item) => item.id === newsId);
if (foundNews) {
news.value = foundNews;
} else {
error.value = 'Новость не найдена';
}
} else {
error.value = 'Не удалось загрузить новость';
}
} catch (err) {
console.error('Ошибка при загрузке новости:', err);
error.value = 'Ошибка при загрузке новости';
} finally {
loading.value = false;
}
};
const goBack = () => {
router.push('/news');
};
const openOriginalUrl = () => {
if (news.value?.url) {
window.open(news.value.url, '_blank');
}
};
const shareNews = async () => {
if (navigator.share && news.value) {
try {
await navigator.share({
title: news.value.title,
text: news.value.rawText?.substring(0, 200) + '...',
url: news.value.url
});
} catch (err) {
console.log('Поделиться отменено или не поддерживается');
}
} else {
// Fallback - копирование URL в буфер обмена
if (news.value?.url) {
try {
await navigator.clipboard.writeText(news.value.url);
// Здесь можно показать уведомление об успешном копировании
console.log('URL скопирован в буфер обмена');
} catch (err) {
console.error('Ошибка при копировании URL:', err);
}
}
}
};
const getSentimentSeverity = (sentiment) => {
switch (sentiment?.toLowerCase()) {
case 'positive':
case 'позитивный':
return 'success';
case 'negative':
case 'негативный':
return 'danger';
case 'neutral':
case 'нейтральный':
return 'info';
default:
return 'secondary';
}
};
const formatDate = (dateString) => {
return NewsService.formatDate(dateString);
};
const formatDateShort = (dateString) => {
return NewsService.formatDateShort(dateString);
};
const truncateUrl = (url) => {
if (!url) return '';
return url.length > 50 ? url.substring(0, 50) + '...' : url;
};
const truncateHash = (hash) => {
if (!hash) return '';
return hash.length > 20 ? hash.substring(0, 20) + '...' : hash;
};
// Жизненный цикл
onMounted(() => {
loadNews();
});
</script>
<template>
<div class="card">
<!-- Загрузка -->
<div v-if="loading" class="text-center py-6">
<ProgressSpinner />
<p class="mt-3 text-color-secondary">Загрузка новости...</p>
</div>
<!-- Ошибка -->
<div v-else-if="error" class="text-center py-6">
<i class="pi pi-exclamation-triangle text-6xl text-red-500 mb-3"></i>
<h3 class="text-xl font-semibold mb-2">Ошибка загрузки</h3>
<p class="text-color-secondary mb-4">{{ error }}</p>
<Button label="Попробовать снова" icon="pi pi-refresh" @click="loadNews" severity="secondary" />
</div>
<!-- Новость не найдена -->
<div v-else-if="!news" class="text-center py-6">
<i class="pi pi-inbox text-6xl text-color-secondary mb-3"></i>
<h3 class="text-xl font-semibold mb-2">Новость не найдена</h3>
<p class="text-color-secondary mb-4">Запрашиваемая новость не существует или была удалена</p>
<Button label="Вернуться к новостям" icon="pi pi-arrow-left" @click="goBack" severity="info" />
</div>
<!-- Контент новости -->
<div v-else>
<!-- Заголовок и метаданные -->
<div class="mb-4">
<div class="flex justify-content-between align-items-start mb-3">
<Button icon="pi pi-arrow-left" label="Назад к новостям" @click="goBack" severity="secondary" text />
<div class="flex gap-2">
<Button icon="pi pi-external-link" label="Оригинал" @click="openOriginalUrl" severity="info" size="small" />
<Button icon="pi pi-share" @click="shareNews" severity="secondary" size="small" v-tooltip.top="'Поделиться'" />
</div>
</div>
<div class="flex flex-wrap align-items-center gap-2 mb-3">
<Tag :value="news.sourceName" severity="info" class="text-sm" />
<Tag v-if="news.category" :value="news.category" severity="success" class="text-sm" />
<div class="flex align-items-center text-sm text-color-secondary">
<i class="pi pi-calendar mr-1"></i>
{{ formatDate(news.publishedAt) }}
</div>
<div class="flex align-items-center text-sm text-color-secondary">
<i class="pi pi-clock mr-1"></i>
Добавлено: {{ formatDateShort(news.addedAt) }}
</div>
</div>
<h1 class="text-3xl font-bold line-height-3 mb-4">{{ news.title }}</h1>
</div>
<!-- Аналитика (если доступна) -->
<div v-if="hasAnalytics" class="mb-4">
<Card>
<template #title>
<div class="flex align-items-center">
<i class="pi pi-chart-line mr-2"></i>
Аналитика
</div>
</template>
<template #content>
<div class="grid">
<div v-if="news.analytics.summary" class="col-12">
<h4 class="font-semibold mb-2">Краткое содержание:</h4>
<p class="text-color-secondary line-height-3">{{ news.analytics.summary }}</p>
</div>
<div v-if="news.analytics.sentiment" class="col-12 md:col-6">
<h4 class="font-semibold mb-2">Тональность:</h4>
<Tag :value="news.analytics.sentiment" :severity="getSentimentSeverity(news.analytics.sentiment)" />
</div>
<div v-if="news.analytics.tags && news.analytics.tags.length > 0" class="col-12 md:col-6">
<h4 class="font-semibold mb-2">Теги:</h4>
<div class="flex flex-wrap gap-1">
<Tag v-for="tag in news.analytics.tags" :key="tag" :value="tag" severity="secondary" class="text-xs" />
</div>
</div>
<div v-if="news.analytics.entities && Object.keys(news.analytics.entities).length > 0" class="col-12">
<h4 class="font-semibold mb-2">Сущности:</h4>
<div class="flex flex-wrap gap-2">
<div v-for="(count, entity) in news.analytics.entities" :key="entity" class="p-2 border-round bg-gray-50 text-sm">
<span class="font-medium">{{ entity }}</span>
<span class="text-color-secondary ml-1">({{ count }})</span>
</div>
</div>
</div>
</div>
</template>
</Card>
</div>
<!-- Основной текст -->
<Card>
<template #title>
<div class="flex align-items-center">
<i class="pi pi-file-text mr-2"></i>
Содержание
</div>
</template>
<template #content>
<div class="prose max-w-none">
<div v-if="news.rawText" class="whitespace-pre-wrap line-height-3">
{{ news.rawText }}
</div>
<div v-else class="text-center py-4 text-color-secondary">
<i class="pi pi-file-o text-4xl mb-2"></i>
<p>Текст новости недоступен</p>
</div>
</div>
</template>
</Card>
<!-- Техническая информация -->
<Card class="mt-4">
<template #title>
<div class="flex align-items-center">
<i class="pi pi-info-circle mr-2"></i>
Техническая информация
</div>
</template>
<template #content>
<div class="grid">
<div class="col-12 md:col-6">
<div class="space-y-2">
<div class="flex justify-content-between">
<span class="font-medium text-sm">ID:</span>
<span class="text-sm font-mono">{{ news.id }}</span>
</div>
<div class="flex justify-content-between">
<span class="font-medium text-sm">URL:</span>
<a :href="news.url" target="_blank" class="text-sm text-blue-600 hover:text-blue-800">
{{ truncateUrl(news.url) }}
</a>
</div>
<div class="flex justify-content-between">
<span class="font-medium text-sm">Хеш:</span>
<span class="text-sm font-mono">{{ truncateHash(news.hash) }}</span>
</div>
</div>
</div>
<div class="col-12 md:col-6">
<div class="space-y-2">
<div class="flex justify-content-between">
<span class="font-medium text-sm">Дата публикации:</span>
<span class="text-sm">{{ formatDate(news.publishedAt) }}</span>
</div>
<div class="flex justify-content-between">
<span class="font-medium text-sm">Дата добавления:</span>
<span class="text-sm">{{ formatDate(news.addedAt) }}</span>
</div>
<div class="flex justify-content-between">
<span class="font-medium text-sm">Источник:</span>
<span class="text-sm">{{ news.sourceName }}</span>
</div>
</div>
</div>
</div>
</template>
</Card>
<!-- Действия -->
<div class="flex justify-content-center gap-2 mt-4">
<Button label="Вернуться к новостям" icon="pi pi-arrow-left" @click="goBack" severity="secondary" />
<Button label="Открыть оригинал" icon="pi pi-external-link" @click="openOriginalUrl" severity="info" />
<Button label="Поделиться" icon="pi pi-share" @click="shareNews" severity="success" />
</div>
</div>
</div>
</template>
<style scoped>
.prose {
max-width: none;
}
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}
.line-height-3 {
line-height: 1.75;
}
</style>