951 lines
21 KiB
Vue
951 lines
21 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { newsApi, categoryApi } from '@/api'
|
|
import type { NewsStatistics, CategoryDto, NewsDto } from '@/types/api'
|
|
import { usePagination } from '@/composables/usePagination'
|
|
|
|
const router = useRouter()
|
|
|
|
// 统计数据
|
|
const statistics = ref<NewsStatistics | null>(null)
|
|
const loadingStats = ref(false)
|
|
|
|
// 分类列表
|
|
const categories = ref<CategoryDto[]>([])
|
|
|
|
// 最新新闻
|
|
const {
|
|
data: latestNews,
|
|
loading: newsLoading,
|
|
fetch: fetchLatestNews
|
|
} = usePagination(newsApi.getLatest.bind(newsApi), { page: 1, size: 5 })
|
|
|
|
// 分类统计(计算属性)
|
|
const categoryStats = computed(() => {
|
|
if (!statistics.value) return []
|
|
return Object.entries(statistics.value.categoryStats)
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
})
|
|
|
|
// 来源统计(计算属性)
|
|
const sourceStats = computed(() => {
|
|
if (!statistics.value) return []
|
|
return Object.entries(statistics.value.sourceStats)
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
})
|
|
|
|
// 格式化数字
|
|
function formatNumber(num: number): string {
|
|
if (num >= 10000) {
|
|
return (num / 10000).toFixed(1) + 'w'
|
|
} else if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'k'
|
|
}
|
|
return num.toString()
|
|
}
|
|
|
|
// 格式化时间
|
|
function formatTime(timeStr: string): string {
|
|
const date = new Date(timeStr)
|
|
const now = new Date()
|
|
const diff = now.getTime() - date.getTime()
|
|
|
|
const minutes = Math.floor(diff / 60000)
|
|
const hours = Math.floor(diff / 3600000)
|
|
const days = Math.floor(diff / 86400000)
|
|
|
|
if (minutes < 1) return '刚刚'
|
|
if (minutes < 60) return `${minutes}分钟前`
|
|
if (hours < 24) return `${hours}小时前`
|
|
if (days < 7) return `${days}天前`
|
|
|
|
return date.toLocaleDateString('zh-CN')
|
|
}
|
|
|
|
// 跳转到新闻详情
|
|
function goToNewsDetail(id: number) {
|
|
router.push(`/news/${id}`)
|
|
}
|
|
|
|
// 初始化数据
|
|
onMounted(async () => {
|
|
// 获取统计数据
|
|
loadingStats.value = true
|
|
try {
|
|
statistics.value = await newsApi.getStatistics()
|
|
} catch (error) {
|
|
console.error('获取统计数据失败:', error)
|
|
} finally {
|
|
loadingStats.value = false
|
|
}
|
|
|
|
// 获取分类列表
|
|
try {
|
|
categories.value = await categoryApi.getWithCount()
|
|
} catch (error) {
|
|
console.error('获取分类列表失败:', error)
|
|
}
|
|
|
|
// 获取最新新闻
|
|
try {
|
|
await fetchLatestNews()
|
|
} catch (error) {
|
|
console.error('获取最新新闻失败:', error)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="dashboard">
|
|
<!-- 统计卡片网格 -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card" data-aos="fade-up" data-aos-delay="0">
|
|
<div class="stat-decoration"></div>
|
|
<div class="stat-header">
|
|
<span class="stat-label">总新闻数</span>
|
|
<div class="stat-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
<div class="stat-value">{{ statistics ? formatNumber(statistics.totalNews) : '-' }}</div>
|
|
<div class="stat-icon-wrapper">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" data-aos="fade-up" data-aos-delay="100">
|
|
<div class="stat-decoration"></div>
|
|
<div class="stat-header">
|
|
<span class="stat-label">分类数量</span>
|
|
<div class="stat-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
<div class="stat-value">{{ categories.length }}</div>
|
|
<div class="stat-icon-wrapper">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M7 12h10"/>
|
|
<path d="M12 7v10"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" data-aos="fade-up" data-aos-delay="200">
|
|
<div class="stat-decoration"></div>
|
|
<div class="stat-header">
|
|
<span class="stat-label">活跃来源</span>
|
|
<div class="stat-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
<div class="stat-value">{{ sourceStats.length }}</div>
|
|
<div class="stat-icon-wrapper">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" data-aos="fade-up" data-aos-delay="300">
|
|
<div class="stat-decoration"></div>
|
|
<div class="stat-header">
|
|
<span class="stat-label">最近更新</span>
|
|
<div class="stat-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
<div class="stat-value small">{{ latestNews && latestNews.length > 0 && latestNews[0] ? formatTime(latestNews[0].createdAt) : '-' }}</div>
|
|
<div class="stat-icon-wrapper">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 8v4l3 3"/>
|
|
<circle cx="12" cy="12" r="10"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-content">
|
|
<!-- 分类分布 -->
|
|
<div class="chart-section" data-aos="fade-up" data-aos-delay="400">
|
|
<div class="section-header">
|
|
<h3 class="section-title">分类分布</h3>
|
|
<div class="corner-bracket"></div>
|
|
</div>
|
|
<div class="category-list">
|
|
<div v-for="(item, index) in categoryStats" :key="item.name" class="category-item" data-aos="slide-right" :data-aos-delay="500 + index * 50">
|
|
<div class="category-info">
|
|
<span class="category-index">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
<span class="category-name">{{ item.name }}</span>
|
|
<span class="category-count">{{ item.count }}</span>
|
|
</div>
|
|
<div class="category-bar">
|
|
<div class="category-bar-fill" :style="{ width: `${(item.count / statistics!.totalNews * 100)}%` }">
|
|
<div class="bar-pattern"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 来源分布 -->
|
|
<div class="chart-section" data-aos="fade-up" data-aos-delay="450">
|
|
<div class="section-header">
|
|
<h3 class="section-title">来源分布</h3>
|
|
<div class="corner-bracket"></div>
|
|
</div>
|
|
<div class="source-list">
|
|
<div v-for="(item, index) in sourceStats.slice(0, 6)" :key="item.name" class="source-item" data-aos="fade-in" :data-aos-delay="500 + index * 50">
|
|
<div class="source-rank">{{ index + 1 }}</div>
|
|
<div class="source-name">{{ item.name }}</div>
|
|
<div class="source-count">{{ formatNumber(item.count) }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最新新闻 -->
|
|
<div class="latest-section" data-aos="fade-up" data-aos-delay="600">
|
|
<div class="section-header">
|
|
<h3 class="section-title">最新新闻</h3>
|
|
<a class="section-link" @click="router.push('/news')">
|
|
查看全部
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="9 18 15 12 9 6"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
<div v-if="newsLoading" class="loading-state">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div v-else-if="latestNews.length > 0" class="news-list">
|
|
<div v-for="(news, index) in latestNews" :key="news.id" class="news-item" data-aos="slide-up" :data-aos-delay="700 + index * 100" @click="goToNewsDetail(news.id)">
|
|
<div class="news-decoration"></div>
|
|
<div class="news-meta">
|
|
<span class="news-category">{{ news.categoryName || '未分类' }}</span>
|
|
<span class="news-divider">/</span>
|
|
<span class="news-source">{{ news.source || '未知来源' }}</span>
|
|
</div>
|
|
<h4 class="news-title">{{ news.title }}</h4>
|
|
<div class="news-footer">
|
|
<span class="news-time">{{ formatTime(news.createdAt) }}</span>
|
|
<svg class="news-arrow" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="9 18 15 12 9 6"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="empty-state">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
</svg>
|
|
<p>暂无新闻数据</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
|
|
|
:root {
|
|
--font-display: 'Archivo Black', sans-serif;
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
--color-black: #000000;
|
|
--color-white: #ffffff;
|
|
--color-gray-50: #fafafa;
|
|
--color-gray-100: #f5f5f5;
|
|
--color-gray-200: #e5e5e5;
|
|
--color-gray-300: #d4d4d4;
|
|
--color-gray-400: #a3a3a3;
|
|
--color-gray-500: #737373;
|
|
--color-gray-600: #525252;
|
|
--color-gray-700: #404040;
|
|
--color-gray-800: #262626;
|
|
--color-gray-900: #171717;
|
|
}
|
|
|
|
.dark {
|
|
--color-black: #ffffff;
|
|
--color-white: #000000;
|
|
--color-gray-50: #0a0a0a;
|
|
--color-gray-100: #171717;
|
|
--color-gray-200: #262626;
|
|
--color-gray-300: #404040;
|
|
--color-gray-400: #525252;
|
|
--color-gray-500: #737373;
|
|
--color-gray-600: #a3a3a3;
|
|
--color-gray-700: #d4d4d4;
|
|
--color-gray-800: #e5e5e5;
|
|
--color-gray-900: #f5f5f5;
|
|
}
|
|
|
|
.dashboard {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* 统计卡片网格 */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.stat-card {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 1.75rem;
|
|
background: var(--color-white);
|
|
border: 2px solid var(--color-black);
|
|
overflow: hidden;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
var(--color-black) 0px,
|
|
var(--color-black) 2px,
|
|
transparent 2px,
|
|
transparent 8px
|
|
);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 8px 8px 0 var(--color-black);
|
|
}
|
|
|
|
.dark .stat-card {
|
|
background: var(--color-gray-100);
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.dark .stat-card::before {
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
var(--color-white) 0px,
|
|
var(--color-white) 2px,
|
|
transparent 2px,
|
|
transparent 8px
|
|
);
|
|
}
|
|
|
|
.dark .stat-card:hover {
|
|
box-shadow: 8px 8px 0 var(--color-white);
|
|
}
|
|
|
|
.stat-decoration {
|
|
position: absolute;
|
|
top: -20px;
|
|
right: -20px;
|
|
width: 80px;
|
|
height: 80px;
|
|
background: repeating-linear-gradient(
|
|
45deg,
|
|
transparent,
|
|
transparent 4px,
|
|
var(--color-gray-200) 4px,
|
|
var(--color-gray-200) 8px
|
|
);
|
|
opacity: 0.5;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.stat-card:hover .stat-decoration {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stat-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.stat-dots {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.stat-dots span {
|
|
width: 4px;
|
|
height: 4px;
|
|
background: var(--color-black);
|
|
border-radius: 50%;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
.stat-dots span:nth-child(2) {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
.stat-dots span:nth-child(3) {
|
|
animation-delay: 0.4s;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.3;
|
|
}
|
|
}
|
|
|
|
.dark .stat-dots span {
|
|
background: var(--color-white);
|
|
}
|
|
|
|
.stat-value {
|
|
font-family: var(--font-display);
|
|
font-size: 3rem;
|
|
font-weight: 400;
|
|
line-height: 1;
|
|
color: var(--color-black);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.stat-value.small {
|
|
font-size: 1.5rem;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.dark .stat-value {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.stat-icon-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 48px;
|
|
height: 48px;
|
|
background: var(--color-black);
|
|
color: var(--color-white);
|
|
border-radius: 8px;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.stat-card:hover .stat-icon-wrapper {
|
|
transform: rotate(5deg);
|
|
}
|
|
|
|
.dark .stat-icon-wrapper {
|
|
background: var(--color-white);
|
|
color: var(--color-black);
|
|
}
|
|
|
|
/* 仪表板内容区 */
|
|
.dashboard-content {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.chart-section {
|
|
background: var(--color-white);
|
|
border: 2px solid var(--color-black);
|
|
padding: 1.75rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chart-section::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, transparent 50%, var(--color-gray-200) 50%);
|
|
}
|
|
|
|
.dark .chart-section {
|
|
background: var(--color-gray-100);
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.dark .chart-section::before {
|
|
background: linear-gradient(135deg, transparent 50%, var(--color-gray-300) 50%);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.section-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1.5rem;
|
|
font-weight: 400;
|
|
color: var(--color-black);
|
|
margin: 0;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.dark .section-title {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.corner-bracket {
|
|
position: absolute;
|
|
right: 0;
|
|
top: -8px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-right: 3px solid var(--color-black);
|
|
border-bottom: 3px solid var(--color-black);
|
|
}
|
|
|
|
.dark .corner-bracket {
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.section-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: var(--color-black);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.section-link:hover {
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.dark .section-link {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
/* 分类列表 */
|
|
.category-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.category-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.category-item:hover {
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.category-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.category-index {
|
|
font-family: var(--font-display);
|
|
font-size: 1.25rem;
|
|
font-weight: 400;
|
|
color: var(--color-gray-400);
|
|
min-width: 32px;
|
|
}
|
|
|
|
.dark .category-index {
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.category-name {
|
|
flex: 1;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--color-black);
|
|
}
|
|
|
|
.dark .category-name {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.category-count {
|
|
font-family: var(--font-display);
|
|
font-size: 1rem;
|
|
color: var(--color-black);
|
|
}
|
|
|
|
.dark .category-count {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.category-bar {
|
|
height: 6px;
|
|
background: var(--color-gray-200);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.dark .category-bar {
|
|
background: var(--color-gray-300);
|
|
}
|
|
|
|
.category-bar-fill {
|
|
height: 100%;
|
|
background: var(--color-black);
|
|
border-radius: 3px;
|
|
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dark .category-bar-fill {
|
|
background: var(--color-white);
|
|
}
|
|
|
|
.bar-pattern {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
transparent 4px,
|
|
rgba(255, 255, 255, 0.3) 4px,
|
|
rgba(255, 255, 255, 0.3) 8px
|
|
);
|
|
}
|
|
|
|
.dark .bar-pattern {
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
transparent 4px,
|
|
rgba(0, 0, 0, 0.3) 4px,
|
|
rgba(0, 0, 0, 0.3) 8px
|
|
);
|
|
}
|
|
|
|
/* 来源列表 */
|
|
.source-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.source-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
background: var(--color-gray-100);
|
|
border: 1px solid var(--color-gray-300);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.source-item:hover {
|
|
background: var(--color-gray-200);
|
|
transform: translateX(4px);
|
|
border-color: var(--color-black);
|
|
}
|
|
|
|
.dark .source-item {
|
|
background: var(--color-gray-200);
|
|
border-color: var(--color-gray-400);
|
|
}
|
|
|
|
.dark .source-item:hover {
|
|
background: var(--color-gray-300);
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.source-rank {
|
|
font-family: var(--font-display);
|
|
font-size: 1.5rem;
|
|
font-weight: 400;
|
|
color: var(--color-gray-400);
|
|
min-width: 32px;
|
|
}
|
|
|
|
.dark .source-rank {
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.source-name {
|
|
flex: 1;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--color-black);
|
|
}
|
|
|
|
.dark .source-name {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.source-count {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--color-black);
|
|
}
|
|
|
|
.dark .source-count {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
/* 最新新闻 */
|
|
.latest-section {
|
|
background: var(--color-white);
|
|
border: 2px solid var(--color-black);
|
|
padding: 1.75rem;
|
|
position: relative;
|
|
}
|
|
|
|
.dark .latest-section {
|
|
background: var(--color-gray-100);
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.news-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.news-item {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
padding: 1.5rem;
|
|
background: var(--color-gray-50);
|
|
border: 1px solid var(--color-gray-300);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.news-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 0;
|
|
background: var(--color-black);
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.news-item:hover {
|
|
transform: translateX(4px);
|
|
border-color: var(--color-black);
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.news-item:hover::before {
|
|
width: 4px;
|
|
}
|
|
|
|
.dark .news-item {
|
|
background: var(--color-gray-200);
|
|
border-color: var(--color-gray-400);
|
|
}
|
|
|
|
.dark .news-item:hover {
|
|
border-color: var(--color-white);
|
|
}
|
|
|
|
.dark .news-item::before {
|
|
background: var(--color-white);
|
|
}
|
|
|
|
.news-decoration {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: repeating-linear-gradient(
|
|
45deg,
|
|
transparent,
|
|
transparent 2px,
|
|
var(--color-gray-300) 2px,
|
|
var(--color-gray-300) 4px
|
|
);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.news-item:hover .news-decoration {
|
|
opacity: 1;
|
|
}
|
|
|
|
.news-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.news-category {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
padding: 0.25rem 0.5rem;
|
|
background: var(--color-black);
|
|
color: var(--color-white);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.dark .news-category {
|
|
background: var(--color-white);
|
|
color: var(--color-black);
|
|
}
|
|
|
|
.news-divider {
|
|
color: var(--color-gray-500);
|
|
}
|
|
|
|
.news-source {
|
|
font-size: 0.75rem;
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.dark .news-source {
|
|
color: var(--color-gray-400);
|
|
}
|
|
|
|
.news-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1.125rem;
|
|
font-weight: 400;
|
|
color: var(--color-black);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dark .news-title {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
.news-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.news-time {
|
|
font-size: 0.75rem;
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.dark .news-time {
|
|
color: var(--color-gray-400);
|
|
}
|
|
|
|
.news-arrow {
|
|
color: var(--color-black);
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.news-item:hover .news-arrow {
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.dark .news-arrow {
|
|
color: var(--color-white);
|
|
}
|
|
|
|
/* 加载状态 */
|
|
.loading-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 3rem;
|
|
}
|
|
|
|
.spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 3px solid var(--color-gray-300);
|
|
border-top-color: var(--color-black);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
.dark .spinner {
|
|
border-color: var(--color-gray-600);
|
|
border-top-color: var(--color-white);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* 空状态 */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--color-gray-600);
|
|
}
|
|
|
|
.empty-state svg {
|
|
margin-bottom: 1.5rem;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.empty-state p {
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|