搜索功能加入

This commit is contained in:
stardrophere
2026-03-13 18:25:38 +08:00
parent 9440b7f590
commit 6aee65af6c
18 changed files with 1545 additions and 103 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ function handleToggle(event: MouseEvent) {
Math.max(y, innerHeight - y)
)
// @ts-ignore: TypeScript 类型可能较旧,忽略 startViewTransition 报错
// @ts-expect-error: TypeScript 类型可能较旧,忽略 startViewTransition 报错
const transition = document.startViewTransition(() => {
themeStore.toggleTheme()
})
@@ -0,0 +1,479 @@
<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 },
},
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>
@@ -1,4 +1,4 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<!-- 图标来源<https://github.com/Templarian/MaterialDesign>遵循 Apache 2.0 许可 -->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -17,3 +17,4 @@
></path>
</svg>
</template>