mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 00:39:21 +08:00
489 lines
13 KiB
Vue
489 lines
13 KiB
Vue
<!-- 统一事件卡片:展示标题、摘要、平台来源、排名轨迹,悬停展开图表 -->
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import VueApexCharts from 'vue3-apexcharts'
|
||
import type { UnifiedEvent } from '@/types/event'
|
||
|
||
const props = defineProps<{
|
||
event: UnifiedEvent
|
||
}>()
|
||
|
||
const router = useRouter()
|
||
|
||
// 当前鼠标悬停的平台行标识
|
||
const hoveredPlatformKey = ref<string | null>(null)
|
||
|
||
function formatRelativeTime(dateStr: string) {
|
||
if (!dateStr) return ''
|
||
if (!dateStr.endsWith('Z') && !dateStr.includes('+')) {
|
||
dateStr += 'Z'
|
||
}
|
||
const now = Date.now()
|
||
const target = new Date(dateStr).getTime()
|
||
const diff = now - target
|
||
const minutes = Math.floor(diff / 60000)
|
||
if (minutes < 1) return '刚刚'
|
||
if (minutes < 60) return `${minutes} 分钟前`
|
||
const hours = Math.floor(minutes / 60)
|
||
if (hours < 24) return `${hours} 小时前`
|
||
const days = Math.floor(hours / 24)
|
||
return `${days} 天前`
|
||
}
|
||
|
||
function getHotLevel(score: number): { label: string; color: string; bg: string } {
|
||
if (score >= 10) return { label: '全网沸腾', color: '#ef4444', bg: 'rgba(239,68,68,0.15)' }
|
||
if (score >= 5) return { label: '高度关注', color: '#f97316', bg: 'rgba(249,115,22,0.15)' }
|
||
if (score >= 3) return { label: '上升中', color: '#3b82f6', bg: 'rgba(59,130,246,0.15)' }
|
||
return { label: '一般关注', color: '#6b7280', bg: 'rgba(107,114,128,0.15)' }
|
||
}
|
||
|
||
const platformIconMap: Record<string, string> = {
|
||
微博热搜: 'fa-brands fa-weibo',
|
||
微博: 'fa-brands fa-weibo',
|
||
知乎热榜: 'fa-brands fa-zhihu',
|
||
知乎: 'fa-brands fa-zhihu',
|
||
百度热搜: 'fa-solid fa-b',
|
||
今日头条: 'fa-solid fa-newspaper',
|
||
抖音热榜: 'fa-brands fa-tiktok',
|
||
抖音: 'fa-brands fa-tiktok',
|
||
'B站热搜': 'fa-brands fa-bilibili',
|
||
'bilibili 热搜': 'fa-brands fa-bilibili',
|
||
华尔街见闻: 'fa-solid fa-chart-line',
|
||
澎湃新闻: 'fa-solid fa-water',
|
||
财联社热门: 'fa-solid fa-coins',
|
||
凤凰网: 'fa-solid fa-feather',
|
||
贴吧: 'fa-solid fa-comments',
|
||
}
|
||
|
||
const platformColorMap: Record<string, string> = {
|
||
微博热搜: '#e6162d',
|
||
微博: '#e6162d',
|
||
知乎热榜: '#0066ff',
|
||
知乎: '#0066ff',
|
||
百度热搜: '#306cff',
|
||
今日头条: '#ff0000',
|
||
抖音热榜: '#000000',
|
||
抖音: '#000000',
|
||
'B站热搜': '#fb7299',
|
||
'bilibili 热搜': '#fb7299',
|
||
华尔街见闻: '#d4a853',
|
||
澎湃新闻: '#1e6cff',
|
||
财联社热门: '#c41230',
|
||
凤凰网: '#f8b500',
|
||
贴吧: '#4e6ef2',
|
||
}
|
||
|
||
function getPlatformIcon(name: string): string {
|
||
return platformIconMap[name] || 'fa-solid fa-globe'
|
||
}
|
||
|
||
function getPlatformColor(name: string): string {
|
||
return platformColorMap[name] || 'var(--text-secondary)'
|
||
}
|
||
|
||
function getRankingTrend(p: { current_ranking: number | null; ranking_history: number[] }) {
|
||
const rank = p.current_ranking
|
||
if (!rank) return { icon: 'fa-solid fa-minus', color: 'var(--text-secondary)', text: '' }
|
||
|
||
const h = p.ranking_history
|
||
if (h.length < 2) return { icon: 'fa-solid fa-minus', color: '#f97316', text: `TOP ${rank}` }
|
||
|
||
const prev = h[h.length - 2]
|
||
const curr = h[h.length - 1]
|
||
if (prev === undefined || curr === undefined) return { icon: 'fa-solid fa-minus', color: '#f97316', text: `TOP ${rank}` }
|
||
const diff = prev - curr
|
||
|
||
if (diff > 0) {
|
||
return { icon: 'fa-solid fa-arrow-trend-up', color: '#ef4444', text: `TOP ${rank} ↑${diff}` }
|
||
}
|
||
if (diff < 0) {
|
||
return { icon: 'fa-solid fa-arrow-trend-down', color: '#10b981', text: `TOP ${rank} ↓${Math.abs(diff)}` }
|
||
}
|
||
return { icon: 'fa-solid fa-equals', color: '#f97316', text: `TOP ${rank}` }
|
||
}
|
||
|
||
function getRankingChartOptions(history: number[], platformColor: string) {
|
||
return {
|
||
series: [{ name: '排名', data: history }],
|
||
chart: {
|
||
type: 'area' as const,
|
||
height: 56,
|
||
sparkline: { enabled: true },
|
||
animations: { enabled: true, easing: 'easeinout' as const, speed: 400 },
|
||
events: {
|
||
mounted: (chartContext: any) => {
|
||
chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
|
||
},
|
||
updated: (chartContext: any) => {
|
||
chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
|
||
}
|
||
}
|
||
},
|
||
stroke: { curve: 'smooth' as const, width: 2 },
|
||
fill: {
|
||
type: 'gradient',
|
||
gradient: {
|
||
shadeIntensity: 1,
|
||
opacityFrom: 0.4,
|
||
opacityTo: 0.05,
|
||
stops: [0, 90, 100],
|
||
colorStops: [
|
||
{ offset: 0, color: platformColor, opacity: 0.3 },
|
||
{ offset: 100, color: platformColor, opacity: 0 },
|
||
],
|
||
},
|
||
},
|
||
yaxis: { reversed: true },
|
||
colors: [platformColor],
|
||
tooltip: {
|
||
theme: 'dark',
|
||
fixed: { enabled: false },
|
||
x: { show: false },
|
||
y: { title: { formatter: () => '第' }, formatter: (val: number) => `${val} 名` },
|
||
},
|
||
}
|
||
}
|
||
|
||
function platformKey(eventId: number, index: number, prefix: string = ''): string {
|
||
return prefix ? `${prefix}-${eventId}-${index}` : `${eventId}-${index}`
|
||
}
|
||
|
||
function goToDashboard() {
|
||
router.push({
|
||
name: 'dashboard',
|
||
query: { event: props.event.event_id }
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<article
|
||
class="event-card"
|
||
:class="{ 'is-hot': event.hot_score >= 50 }"
|
||
>
|
||
<div class="card-top">
|
||
<div class="tags-row">
|
||
<span
|
||
class="hot-badge"
|
||
:style="{ color: getHotLevel(event.hot_score).color, background: getHotLevel(event.hot_score).bg }"
|
||
>
|
||
{{ getHotLevel(event.hot_score).label }} ({{ event.hot_score }})
|
||
</span>
|
||
<span v-for="tag in event.tags.slice(0, 3)" :key="tag" class="topic-tag">{{ tag }}</span>
|
||
</div>
|
||
<div class="event-time-range" title="左侧为首次发现时间,右侧为最后活跃时间">
|
||
<span>{{ formatRelativeTime(event.created_at) }}</span>
|
||
<template v-if="formatRelativeTime(event.created_at) !== formatRelativeTime(event.last_active_at)">
|
||
<i class="fa-solid fa-arrow-right time-arrow"></i>
|
||
<span class="active-time">{{ formatRelativeTime(event.last_active_at) }}</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="card-title" @click="goToDashboard" style="cursor: pointer;">
|
||
{{ event.unified_title }}
|
||
</h3>
|
||
|
||
<div v-if="event.summary" class="ai-summary">
|
||
<i class="fa-solid fa-wand-magic-sparkles summary-icon"></i>
|
||
<p>
|
||
<span class="summary-label">AI 全局洞察:</span>
|
||
{{ event.summary }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="event.platforms.length > 0" class="platforms-list">
|
||
<div
|
||
v-for="(p, pIdx) in event.platforms"
|
||
:key="pIdx"
|
||
class="platform-block"
|
||
@mouseenter="hoveredPlatformKey = platformKey(event.event_id, pIdx)"
|
||
@mouseleave="hoveredPlatformKey = null"
|
||
>
|
||
<div class="platform-row">
|
||
<div class="platform-info">
|
||
<i
|
||
:class="getPlatformIcon(p.platform_name)"
|
||
:style="{ color: getPlatformColor(p.platform_name) }"
|
||
></i>
|
||
<a
|
||
v-if="p.url"
|
||
:href="p.url"
|
||
target="_blank"
|
||
rel="noopener"
|
||
class="platform-headline platform-link"
|
||
>
|
||
{{ p.platform_name }}:{{ p.headline }}
|
||
</a>
|
||
<span v-else class="platform-headline">{{ p.platform_name }}:{{ p.headline }}</span>
|
||
</div>
|
||
<span v-if="p.current_ranking" class="ranking-badge" :style="{ color: getRankingTrend(p).color }">
|
||
<i :class="getRankingTrend(p).icon"></i> {{ getRankingTrend(p).text }}
|
||
</span>
|
||
</div>
|
||
|
||
<transition name="chart-expand">
|
||
<div
|
||
v-if="
|
||
hoveredPlatformKey === platformKey(event.event_id, pIdx) &&
|
||
p.ranking_history && p.ranking_history.length > 1
|
||
"
|
||
class="inline-chart"
|
||
>
|
||
<p class="chart-label">
|
||
<i
|
||
:class="getPlatformIcon(p.platform_name)"
|
||
:style="{ color: getPlatformColor(p.platform_name) }"
|
||
></i>
|
||
{{ p.platform_name }}「{{ p.headline.length > 16 ? p.headline.slice(0, 16) + '...' : p.headline }}」排名轨迹
|
||
</p>
|
||
<VueApexCharts
|
||
type="area"
|
||
height="56"
|
||
:options="getRankingChartOptions(p.ranking_history, getPlatformColor(p.platform_name))"
|
||
:series="getRankingChartOptions(p.ranking_history, getPlatformColor(p.platform_name)).series"
|
||
/>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.event-card {
|
||
background: var(--bg-surface);
|
||
backdrop-filter: var(--backdrop-blur);
|
||
-webkit-backdrop-filter: var(--backdrop-blur);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-xl);
|
||
padding: 24px;
|
||
margin-bottom: 20px;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: var(--shadow-sm);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.event-card:hover {
|
||
border-color: var(--brand-primary-alpha);
|
||
box-shadow: var(--shadow-xl);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.event-card.is-hot {
|
||
border-left: 4px solid #ef4444;
|
||
}
|
||
|
||
.card-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.tags-row {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.hot-badge {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.topic-tag {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
background: var(--bg-input);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.event-time-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.time-arrow {
|
||
font-size: 10px;
|
||
color: var(--text-placeholder);
|
||
}
|
||
|
||
.active-time {
|
||
color: var(--brand-primary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
margin: 0 0 12px;
|
||
line-height: 1.4;
|
||
color: var(--text-color);
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.card-title:hover {
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.ai-summary {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border-radius: var(--radius-lg);
|
||
background: linear-gradient(145deg, var(--brand-primary-alpha), transparent);
|
||
border: 1px solid rgba(99, 102, 241, 0.1);
|
||
margin-bottom: 20px;
|
||
box-shadow: inset 0 2px 4px rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.summary-icon {
|
||
color: var(--brand-primary);
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.ai-summary p {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: var(--text-secondary);
|
||
margin: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.summary-label {
|
||
font-weight: 600;
|
||
background: linear-gradient(to right, #a78bfa, #818cf8);
|
||
-webkit-background-clip: text;
|
||
color: transparent;
|
||
}
|
||
|
||
.platforms-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.platform-block {
|
||
border-radius: var(--radius-md);
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.platform-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-radius: var(--radius-md);
|
||
background: var(--bg-input);
|
||
border: 1px solid transparent;
|
||
font-size: 14px;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.platform-block:hover .platform-row {
|
||
background: var(--bg-surface);
|
||
border-color: var(--border-subtle);
|
||
transform: scale(1.01);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.platform-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.platform-info i {
|
||
font-size: 16px;
|
||
width: 20px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.platform-headline {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.platform-link {
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: color 0.15s;
|
||
}
|
||
|
||
.platform-link:hover {
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.ranking-badge {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
margin-left: 8px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.inline-chart {
|
||
padding: 8px 12px 10px;
|
||
background: var(--bg-input);
|
||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||
margin-top: -2px;
|
||
}
|
||
|
||
.chart-label {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
margin: 0 0 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.chart-label i {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.chart-expand-enter-active {
|
||
transition: all 0.25s ease-out;
|
||
}
|
||
|
||
.chart-expand-leave-active {
|
||
transition: all 0.15s ease-in;
|
||
}
|
||
|
||
.chart-expand-enter-from,
|
||
.chart-expand-leave-to {
|
||
opacity: 0;
|
||
max-height: 0;
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.chart-expand-enter-to,
|
||
.chart-expand-leave-from {
|
||
opacity: 1;
|
||
max-height: 120px;
|
||
}
|
||
</style>
|