feat: 优化ui 结构

This commit is contained in:
shenjianZ 2026-01-19 15:21:29 +08:00
parent d02e8d65e0
commit 4b68e6ba3d
5 changed files with 1628 additions and 406 deletions

View File

@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(cargo check:*)",
"Bash(cargo:*)"
"Bash(cargo:*)",
"Skill(frontend-design:frontend-design)"
]
}
}

View File

@ -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>

View File

@ -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);
}
/* 内容区域 */

View File

@ -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>

View File

@ -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>