Compare commits
1 Commits
master
...
forntend-d
| Author | SHA1 | Date |
|---|---|---|
|
|
4b68e6ba3d |
|
|
@ -2,7 +2,8 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cargo check:*)",
|
||||
"Bash(cargo:*)"
|
||||
"Bash(cargo:*)",
|
||||
"Skill(frontend-design:frontend-design)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ const summary = computed(() => {
|
|||
|
||||
<template>
|
||||
<div class="news-card">
|
||||
<!-- 装饰元素 -->
|
||||
<div class="card-decoration"></div>
|
||||
<div class="corner-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<div class="news-card-category">
|
||||
<span class="category-badge">{{ news.categoryName || '未分类' }}</span>
|
||||
|
|
@ -46,10 +53,13 @@ const summary = computed(() => {
|
|||
<!-- 新闻摘要 -->
|
||||
<p class="news-card-summary">{{ summary }}</p>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="card-divider"></div>
|
||||
|
||||
<!-- 新闻元信息 -->
|
||||
<div class="news-card-meta">
|
||||
<span class="meta-item source">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
|
|
@ -57,71 +67,230 @@ const summary = computed(() => {
|
|||
{{ news.source || '未知来源' }}
|
||||
</span>
|
||||
<span v-if="news.author" class="meta-item author">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
{{ news.author }}
|
||||
</span>
|
||||
<span class="meta-item time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
{{ formattedTime }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 箭头 -->
|
||||
<div class="card-arrow">
|
||||
<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>
|
||||
</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;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
background: var(--color-white);
|
||||
border: 2px solid var(--color-black);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.news-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--color-black) 0px,
|
||||
var(--color-black) 2px,
|
||||
transparent 2px,
|
||||
transparent 6px
|
||||
);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
box-shadow: 0 4px 20px rgb(0 0 0 / 0.08);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 8px 8px 0 var(--color-black);
|
||||
}
|
||||
|
||||
.news-card:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.dark .news-card {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .news-card::before {
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--color-white) 0px,
|
||||
var(--color-white) 2px,
|
||||
transparent 2px,
|
||||
transparent 6px
|
||||
);
|
||||
}
|
||||
|
||||
.dark .news-card:hover {
|
||||
box-shadow: 8px 8px 0 var(--color-white);
|
||||
}
|
||||
|
||||
.card-decoration {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 3px,
|
||||
var(--color-gray-200) 3px,
|
||||
var(--color-gray-200) 6px
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.news-card:hover .card-decoration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dark .card-decoration {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 3px,
|
||||
var(--color-gray-300) 3px,
|
||||
var(--color-gray-300) 6px
|
||||
);
|
||||
}
|
||||
|
||||
.corner-dots {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.corner-dots span {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--color-gray-400);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.corner-dots span:nth-child(2) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .corner-dots span {
|
||||
background: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.news-card-category {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 4px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.dark .category-badge {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.news-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-black);
|
||||
margin: 0 0 0.75rem 0;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dark .news-card-title {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.news-card-summary {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-gray-600);
|
||||
margin: 0 0 1rem 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
|
|
@ -131,35 +300,89 @@ const summary = computed(() => {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.dark .news-card-summary {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-gray-300) 20%,
|
||||
var(--color-gray-300) 80%,
|
||||
transparent
|
||||
);
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
.dark .card-divider {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-gray-600) 20%,
|
||||
var(--color-gray-600) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.news-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
gap: 0.875rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
gap: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-item svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dark .meta-item {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.meta-item.source {
|
||||
color: hsl(var(--foreground));
|
||||
color: var(--color-black);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meta-item.author {
|
||||
color: hsl(var(--muted-foreground));
|
||||
.dark .meta-item.source {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.meta-item.time {
|
||||
margin-left: auto;
|
||||
.card-arrow {
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.news-card:hover .card-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.dark .card-arrow {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -58,20 +58,13 @@ onMounted(() => {
|
|||
isDark.value = true
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
// 加载用户信息
|
||||
userStore.loadUserInfo()
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换侧边栏
|
||||
*/
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
if (isDark.value) {
|
||||
|
|
@ -83,24 +76,15 @@ function toggleTheme() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户菜单
|
||||
*/
|
||||
function toggleUserMenu() {
|
||||
userMenuOpen.value = !userMenuOpen.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登出
|
||||
*/
|
||||
function handleLogout() {
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定路径
|
||||
*/
|
||||
function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
|
@ -113,16 +97,18 @@ function navigateTo(path: string) {
|
|||
<!-- Logo 区域 -->
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<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="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
|
||||
<path d="M18 14h-8"/>
|
||||
<path d="M15 18h-5"/>
|
||||
<path d="M10 6h8v4h-8V6Z"/>
|
||||
</svg>
|
||||
<span v-show="!sidebarCollapsed" class="logo-text">News Classifier</span>
|
||||
<div class="logo-icon">
|
||||
<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="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
|
||||
<path d="M18 14h-8"/>
|
||||
<path d="M15 18h-5"/>
|
||||
<path d="M10 6h8v4h-8V6Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span v-show="!sidebarCollapsed" class="logo-text">NEWS<br/>CLASSIFIER</span>
|
||||
</div>
|
||||
<button class="collapse-btn" @click="toggleSidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M9 3v18"/>
|
||||
<path d="m14 9 3 3-3 3"/>
|
||||
|
|
@ -134,21 +120,23 @@ function navigateTo(path: string) {
|
|||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<div v-for="item in navigation" :key="item.path" class="nav-item" :class="{ active: route.path.startsWith(item.path) }" @click="navigateTo(item.path)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path :d="item.icon"/>
|
||||
</svg>
|
||||
<span v-show="!sidebarCollapsed">{{ item.name }}</span>
|
||||
<div v-show="!sidebarCollapsed" class="nav-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员菜单 -->
|
||||
<div v-if="userStore.isAdmin" class="nav-section admin-section">
|
||||
<div v-show="!sidebarCollapsed" class="nav-section-title">管理</div>
|
||||
<div v-show="!sidebarCollapsed" class="nav-section-title">ADMIN</div>
|
||||
<div v-for="item in adminNavigation" :key="item.path" class="nav-item" :class="{ active: route.path.startsWith(item.path) }" @click="navigateTo(item.path)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path :d="item.icon"/>
|
||||
</svg>
|
||||
<span v-show="!sidebarCollapsed">{{ item.name }}</span>
|
||||
<div v-show="!sidebarCollapsed" class="nav-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -165,7 +153,7 @@ function navigateTo(path: string) {
|
|||
<div class="header-right">
|
||||
<!-- 主题切换 -->
|
||||
<button class="icon-btn" @click="toggleTheme" title="切换主题">
|
||||
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
|
|
@ -176,22 +164,22 @@ function navigateTo(path: string) {
|
|||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<div class="user-menu" v-click-outside="() => userMenuOpen = false">
|
||||
<div class="user-menu">
|
||||
<button class="user-btn" @click="toggleUserMenu">
|
||||
<div class="user-avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="user-name">{{ userStore.userInfo?.username || '用户' }}</span>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -235,74 +223,179 @@ function navigateTo(path: string) {
|
|||
</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;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: hsl(var(--background));
|
||||
background: var(--color-gray-50);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.dark .main-layout {
|
||||
background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: hsl(var(--sidebar));
|
||||
border-right: 1px solid hsl(var(--sidebar-border));
|
||||
background: var(--color-white);
|
||||
border-right: 2px solid var(--color-black);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
var(--color-black) 0px,
|
||||
var(--color-black) 2px,
|
||||
transparent 2px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
.dark .sidebar {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .sidebar::before {
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
var(--color-white) 0px,
|
||||
var(--color-white) 2px,
|
||||
transparent 2px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 70px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--sidebar-border));
|
||||
padding: 1.5rem 1rem;
|
||||
border-bottom: 1px solid var(--color-gray-300);
|
||||
}
|
||||
|
||||
.dark .sidebar-header {
|
||||
border-color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: hsl(var(--sidebar-primary));
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
.logo-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dark .logo-icon {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-weight: 600;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
padding: 0.5rem;
|
||||
color: hsl(var(--sidebar-foreground));
|
||||
color: var(--color-gray-600);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background: hsl(var(--sidebar-accent));
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .collapse-btn {
|
||||
color: var(--color-gray-500);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .collapse-btn:hover {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0.75rem;
|
||||
padding: 1.5rem 0.75rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-section:last-child {
|
||||
|
|
@ -311,42 +404,84 @@ function navigateTo(path: string) {
|
|||
|
||||
.nav-section-title {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-gray-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
border-top: 1px solid hsl(var(--sidebar-border));
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-gray-300);
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.dark .admin-section {
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
color: hsl(var(--sidebar-foreground));
|
||||
color: var(--color-gray-700);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
transition: all 0.2s;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: hsl(var(--sidebar-accent));
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .nav-item {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.dark .nav-item:hover {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: hsl(var(--sidebar-accent));
|
||||
color: hsl(var(--sidebar-primary));
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .nav-item.active {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-item span {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-indicator {
|
||||
margin-left: auto;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: currentColor;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
|
|
@ -358,15 +493,18 @@ function navigateTo(path: string) {
|
|||
/* 顶部栏 */
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
background: hsl(var(--card));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
overflow: visible;
|
||||
isolation: isolate;
|
||||
background: var(--color-white);
|
||||
border-bottom: 2px solid var(--color-black);
|
||||
}
|
||||
|
||||
.dark .header {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
|
|
@ -375,31 +513,50 @@ function navigateTo(path: string) {
|
|||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
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 .page-title {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
padding: 0.625rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-600);
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--foreground));
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .icon-btn {
|
||||
color: var(--color-gray-500);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .icon-btn:hover {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
|
|
@ -412,15 +569,26 @@ function navigateTo(path: string) {
|
|||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: hsl(var(--accent));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-gray-100);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-btn:hover {
|
||||
background: hsl(var(--accent) / 0.8);
|
||||
background: var(--color-gray-200);
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .user-btn {
|
||||
background: var(--color-gray-200);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .user-btn:hover {
|
||||
background: var(--color-gray-300);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
|
|
@ -429,48 +597,74 @@ function navigateTo(path: string) {
|
|||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 50%;
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dark .user-avatar {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-black);
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dark .user-name {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
width: 240px;
|
||||
background: var(--popover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 10px 40px -10px rgb(0 0 0 / 0.2);
|
||||
width: 260px;
|
||||
background: var(--color-white);
|
||||
border: 2px solid var(--color-black);
|
||||
border-radius: 8px;
|
||||
box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .user-dropdown {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
box-shadow: 8px 8px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--color-gray-300);
|
||||
}
|
||||
|
||||
.dark .dropdown-header {
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dropdown-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .dropdown-avatar {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dropdown-info {
|
||||
|
|
@ -479,20 +673,33 @@ function navigateTo(path: string) {
|
|||
}
|
||||
|
||||
.dropdown-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .dropdown-name {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.dropdown-role {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-600);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.dark .dropdown-role {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: hsl(var(--border));
|
||||
margin: 0 1rem;
|
||||
background: var(--color-gray-300);
|
||||
}
|
||||
|
||||
.dark .dropdown-divider {
|
||||
background: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
|
@ -500,21 +707,35 @@ function navigateTo(path: string) {
|
|||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-black);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: hsl(var(--accent));
|
||||
background: var(--color-gray-100);
|
||||
}
|
||||
|
||||
.dark .dropdown-item {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .dropdown-item:hover {
|
||||
background: var(--color-gray-200);
|
||||
}
|
||||
|
||||
.dropdown-item svg {
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.dark .dropdown-item svg {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
|
|
|
|||
|
|
@ -100,86 +100,116 @@ onMounted(async () => {
|
|||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 统计卡片 -->
|
||||
<!-- 统计卡片网格 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<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 class="stat-content">
|
||||
<div class="stat-label">总新闻数</div>
|
||||
<div class="stat-value">{{ statistics ? formatNumber(statistics.totalNews) : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon secondary">
|
||||
<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 class="stat-content">
|
||||
<div class="stat-label">分类数量</div>
|
||||
<div class="stat-value">{{ categories.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon accent">
|
||||
<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 class="stat-content">
|
||||
<div class="stat-label">活跃来源</div>
|
||||
<div class="stat-value">{{ sourceStats.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<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 class="stat-content">
|
||||
<div class="stat-label">最近更新</div>
|
||||
<div class="stat-value" style="font-size: 1rem;">{{ latestNews && latestNews.length > 0 && latestNews[0] ? formatTime(latestNews[0].createdAt) : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-content">
|
||||
<!-- 分类分布 -->
|
||||
<div class="chart-section">
|
||||
<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 in categoryStats" :key="item.name" class="category-item">
|
||||
<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>
|
||||
<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">
|
||||
<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 in sourceStats" :key="item.name" class="source-item">
|
||||
<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>
|
||||
|
|
@ -188,22 +218,34 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<!-- 最新新闻 -->
|
||||
<div class="latest-section">
|
||||
<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')">查看全部 →</a>
|
||||
<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 in latestNews" :key="news.id" class="news-item" @click="goToNewsDetail(news.id)">
|
||||
<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-time">{{ formatTime(news.createdAt) }}</div>
|
||||
<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">
|
||||
|
|
@ -218,164 +260,411 @@ onMounted(async () => {
|
|||
</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: 1.5rem;
|
||||
gap: 2rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* 统计卡片网格 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
transition: box-shadow 0.2s;
|
||||
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 {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 8px 8px 0 var(--color-black);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
.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;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.stat-icon.primary {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
.stat-card:hover .stat-icon-wrapper {
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.stat-icon.secondary {
|
||||
background: hsl(var(--secondary) / 0.1);
|
||||
color: hsl(var(--secondary));
|
||||
}
|
||||
|
||||
.stat-icon.accent {
|
||||
background: hsl(var(--accent) / 0.1);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.stat-icon.success {
|
||||
background: hsl(142 76% 96% / 0.5);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.dark .stat-icon.success {
|
||||
background: hsl(142 76% 20% / 0.5);
|
||||
color: hsl(142 76% 60%);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
.dark .stat-icon-wrapper {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
/* 仪表板内容区 */
|
||||
.dashboard-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
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: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
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 {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--primary));
|
||||
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 {
|
||||
text-decoration: underline;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dark .section-link {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
/* 分类列表 */
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
color: hsl(var(--foreground));
|
||||
font-weight: 500;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .category-name {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .category-count {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.category-bar {
|
||||
height: 8px;
|
||||
background: hsl(var(--secondary));
|
||||
border-radius: 4px;
|
||||
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: hsl(var(--primary));
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/* 来源列表 */
|
||||
|
|
@ -387,101 +676,251 @@ onMounted(async () => {
|
|||
|
||||
.source-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: hsl(var(--secondary));
|
||||
border-radius: var(--radius);
|
||||
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;
|
||||
color: hsl(var(--foreground));
|
||||
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: hsl(var(--primary));
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .source-count {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
/* 最新新闻 */
|
||||
.latest-section {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
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: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
padding: 1rem;
|
||||
background: hsl(var(--secondary));
|
||||
border-radius: var(--radius);
|
||||
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: background 0.2s;
|
||||
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 {
|
||||
background: hsl(var(--accent));
|
||||
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;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.news-category {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 4px;
|
||||
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: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.dark .news-source {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 0 0 0.5rem 0;
|
||||
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: hsl(var(--muted-foreground));
|
||||
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: 2rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid hsl(var(--border));
|
||||
border-top-color: hsl(var(--primary));
|
||||
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);
|
||||
|
|
@ -494,17 +933,18 @@ onMounted(async () => {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
padding: 4rem 2rem;
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ const searchKeyword = ref('')
|
|||
|
||||
// 来源列表(从分类中提取或手动定义)
|
||||
const sources = computed(() => {
|
||||
// 可以从新闻中动态获取,这里先定义一些常见的
|
||||
return ['全部', '网易', '36kr', '新浪', '腾讯', '澎湃']
|
||||
})
|
||||
|
||||
|
|
@ -106,14 +105,12 @@ function changeSort(value: string) {
|
|||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 获取分类列表
|
||||
try {
|
||||
categories.value = await categoryApi.getAll()
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
}
|
||||
|
||||
// 获取新闻列表
|
||||
try {
|
||||
await fetchNews()
|
||||
} catch (error) {
|
||||
|
|
@ -121,9 +118,7 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch(filters, () => {
|
||||
// 筛选条件变化时自动刷新
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
|
|
@ -133,13 +128,17 @@ watch(filters, () => {
|
|||
<div class="filter-bar">
|
||||
<!-- 分类筛选 -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">分类</label>
|
||||
<label class="filter-label">
|
||||
<span class="label-icon">▸</span>
|
||||
分类
|
||||
</label>
|
||||
<div class="filter-options">
|
||||
<button
|
||||
class="filter-chip"
|
||||
:class="{ active: !filters.categoryId }"
|
||||
@click="selectCategory(undefined)"
|
||||
>
|
||||
<span class="chip-index">—</span>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -149,14 +148,22 @@ watch(filters, () => {
|
|||
:class="{ active: filters.categoryId === category.id }"
|
||||
@click="selectCategory(category.id)"
|
||||
>
|
||||
<span class="chip-index">{{ category.id }}</span>
|
||||
{{ category.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 来源筛选 -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">来源</label>
|
||||
<div class="filter-group-inline">
|
||||
<label class="filter-label-inline">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
来源
|
||||
</label>
|
||||
<select class="filter-select" @change="(e) => selectSource((e.target as HTMLSelectElement).value)">
|
||||
<option value="全部">全部来源</option>
|
||||
<option v-for="source in sources" :key="source" :value="source" :selected="filters.source === source">
|
||||
|
|
@ -166,8 +173,15 @@ watch(filters, () => {
|
|||
</div>
|
||||
|
||||
<!-- 排序 -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">排序</label>
|
||||
<div class="filter-group-inline">
|
||||
<label class="filter-label-inline">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M7 12h10"/>
|
||||
<path d="M10 18h4"/>
|
||||
</svg>
|
||||
排序
|
||||
</label>
|
||||
<select class="filter-select" @change="(e) => changeSort((e.target as HTMLSelectElement).value)">
|
||||
<option v-for="option in sortOptions" :key="option.value" :value="option.value" :selected="selectedSort === option.value">
|
||||
{{ option.label }}
|
||||
|
|
@ -177,23 +191,27 @@ watch(filters, () => {
|
|||
|
||||
<!-- 搜索 -->
|
||||
<div class="filter-group search-group">
|
||||
<label class="filter-label">
|
||||
<span class="label-icon">▸</span>
|
||||
搜索
|
||||
</label>
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
placeholder="搜索新闻标题或内容..."
|
||||
placeholder="输入关键词搜索..."
|
||||
class="search-input"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<button v-if="searchKeyword" class="search-clear" @click="clearSearch">
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="search-btn" @click="handleSearch">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
|
|
@ -220,10 +238,12 @@ watch(filters, () => {
|
|||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" 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 class="empty-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>
|
||||
<h3>暂无新闻</h3>
|
||||
<p>试试调整筛选条件或搜索关键词</p>
|
||||
</div>
|
||||
|
|
@ -231,12 +251,20 @@ watch(filters, () => {
|
|||
<!-- 分页 -->
|
||||
<div v-if="total > 0" class="pagination">
|
||||
<div class="pagination-info">
|
||||
共 <span class="count">{{ formatNumber(total) }}</span> 条新闻,
|
||||
第 <span class="current">{{ page }}</span> / <span class="total">{{ totalPages }}</span> 页
|
||||
<span class="info-label">总计</span>
|
||||
<span class="count">{{ formatNumber(total) }}</span>
|
||||
<span class="info-divider">/</span>
|
||||
<span class="info-label">第</span>
|
||||
<span class="current">{{ page }}</span>
|
||||
<span class="info-label">页</span>
|
||||
<span class="info-divider">/</span>
|
||||
<span class="info-label">共</span>
|
||||
<span class="total">{{ totalPages }}</span>
|
||||
<span class="info-label">页</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="page-btn" :disabled="!hasPrevPage" @click="prevPage">
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
上一页
|
||||
|
|
@ -249,12 +277,12 @@ watch(filters, () => {
|
|||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
{{ String(p).padStart(2, '0') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="page-btn" :disabled="!hasNextPage" @click="nextPage">
|
||||
下一页
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
|
||||
</button>
|
||||
|
|
@ -264,34 +292,136 @@ watch(filters, () => {
|
|||
</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;
|
||||
}
|
||||
|
||||
.news-list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
gap: 2rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
align-items: flex-end;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-white);
|
||||
border: 2px solid var(--color-black);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-bar::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
|
||||
);
|
||||
}
|
||||
|
||||
.dark .filter-bar {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .filter-bar::before {
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--color-white) 0px,
|
||||
var(--color-white) 2px,
|
||||
transparent 2px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-group-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-gray-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.dark .filter-label {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.filter-label-inline {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-gray-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.dark .filter-label-inline {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
|
|
@ -301,40 +431,116 @@ watch(filters, () => {
|
|||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--secondary));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-black);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-chip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
background: var(--color-black);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
border-color: var(--color-black);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.filter-chip.active::before {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.dark .filter-chip {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .filter-chip:hover {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .filter-chip.active {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .filter-chip.active::before {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.chip-index {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.6;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-gray-50);
|
||||
color: var(--color-black);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background: hsl(var(--accent));
|
||||
.filter-select:hover {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-black);
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
min-width: 120px;
|
||||
.dark .filter-select {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .filter-select:hover {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .filter-select:focus {
|
||||
border-color: var(--color-white);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-group {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
|
|
@ -344,19 +550,35 @@ watch(filters, () => {
|
|||
}
|
||||
|
||||
.search-input {
|
||||
padding: 0.5rem 2.5rem 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
width: 280px;
|
||||
width: 100%;
|
||||
padding: 0.625rem 2.5rem 0.625rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-gray-50);
|
||||
color: var(--color-black);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--ring));
|
||||
box-shadow: 0 0 0 3px hsl(var(--ring) / 0.2);
|
||||
border-color: var(--color-black);
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .search-input {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .search-input:focus {
|
||||
border-color: var(--color-white);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
|
|
@ -365,33 +587,52 @@ watch(filters, () => {
|
|||
padding: 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-500);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
color: hsl(var(--foreground));
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .search-clear:hover {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: all 0.2s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background: hsl(var(--primary) / 0.9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .search-btn {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .search-btn:hover {
|
||||
box-shadow: 4px 4px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
|
|
@ -401,17 +642,27 @@ watch(filters, () => {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 1rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid hsl(var(--border));
|
||||
border-top-color: hsl(var(--primary));
|
||||
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;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dark .spinner {
|
||||
border-color: var(--color-gray-600);
|
||||
border-top-color: var(--color-white);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
|
|
@ -423,8 +674,8 @@ watch(filters, () => {
|
|||
/* 新闻网格 */
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
|
|
@ -433,25 +684,49 @@ watch(filters, () => {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 1rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
padding: 5rem 2rem;
|
||||
background: var(--color-white);
|
||||
border: 2px dashed var(--color-gray-400);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
.dark .empty-state {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-gray-100);
|
||||
border-radius: 50%;
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .empty-icon {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-black);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.dark .empty-state h3 {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.dark .empty-state p {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
|
|
@ -459,24 +734,50 @@ watch(filters, () => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: var(--color-white);
|
||||
border: 2px solid var(--color-black);
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dark .pagination {
|
||||
background: var(--color-gray-100);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.dark .info-label {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.info-divider {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.pagination-info .count,
|
||||
.pagination-info .current,
|
||||
.pagination-info .total {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .pagination-info .count,
|
||||
.dark .pagination-info .current,
|
||||
.dark .pagination-info .total {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
|
|
@ -488,40 +789,59 @@ watch(filters, () => {
|
|||
.page-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-black);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
background: hsl(var(--accent));
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-black);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dark .page-btn {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .page-btn:hover:not(:disabled) {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
min-width: 36px;
|
||||
min-width: 40px;
|
||||
height: 36px;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-black);
|
||||
border: 1px solid var(--color-gray-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
|
|
@ -530,12 +850,29 @@ watch(filters, () => {
|
|||
}
|
||||
|
||||
.page-number:hover {
|
||||
background: hsl(var(--accent));
|
||||
border-color: var(--color-black);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.page-number.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.dark .page-number {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.dark .page-number:hover {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.dark .page-number.active {
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue