initial commit
This commit is contained in:
233
src/components/AnimatedElement.astro
Normal file
233
src/components/AnimatedElement.astro
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
export interface Props {
|
||||
animation?: 'fadeIn' | 'fadeInUp' | 'fadeInDown' | 'slideInLeft' | 'slideInRight' | 'scaleIn' | 'rotateIn' | 'bounceIn';
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
easing?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear';
|
||||
trigger?: 'load' | 'scroll' | 'hover' | 'click';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
animation = 'fadeIn',
|
||||
delay = 0,
|
||||
duration = 600,
|
||||
easing = 'ease-out',
|
||||
trigger = 'load',
|
||||
className = ''
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class={`animate-wrapper ${className}`}
|
||||
data-animation={animation}
|
||||
data-trigger={trigger}
|
||||
style={`--delay: ${delay}ms; --duration: ${duration}ms; --easing: ${easing};`}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.animate-wrapper {
|
||||
animation-duration: var(--duration);
|
||||
animation-timing-function: var(--easing);
|
||||
animation-delay: var(--delay);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* 初始状态 */
|
||||
.animate-wrapper[data-trigger="scroll"] {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.animate-wrapper[data-trigger="hover"] {
|
||||
transition: all var(--duration) var(--easing);
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
.animate-wrapper[data-animation="fadeIn"] {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="fadeInUp"] {
|
||||
animation-name: fadeInUp;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="fadeInDown"] {
|
||||
animation-name: fadeInDown;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="slideInLeft"] {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="slideInRight"] {
|
||||
animation-name: slideInRight;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="scaleIn"] {
|
||||
animation-name: scaleIn;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="rotateIn"] {
|
||||
animation-name: rotateIn;
|
||||
}
|
||||
|
||||
.animate-wrapper[data-animation="bounceIn"] {
|
||||
animation-name: bounceIn;
|
||||
}
|
||||
|
||||
/* 触发状态 */
|
||||
.animate-wrapper.animate-visible {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
/* 关键帧动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(-200deg) scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 悬浮效果 */
|
||||
.animate-wrapper[data-trigger="hover"]:hover {
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
}
|
||||
|
||||
/* 减少动画在移动设备上的使用 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-wrapper {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 滚动触发动画
|
||||
function initScrollAnimations() {
|
||||
const animateElements = document.querySelectorAll('[data-trigger="scroll"]');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-visible');
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
});
|
||||
|
||||
animateElements.forEach(element => {
|
||||
observer.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击触发动画
|
||||
function initClickAnimations() {
|
||||
const clickElements = document.querySelectorAll('[data-trigger="click"]');
|
||||
|
||||
clickElements.forEach(element => {
|
||||
element.addEventListener('click', () => {
|
||||
element.classList.toggle('animate-visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initScrollAnimations();
|
||||
initClickAnimations();
|
||||
});
|
||||
</script>
|
||||
209
src/components/Container.astro
Normal file
209
src/components/Container.astro
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
export interface Props {
|
||||
variant?: 'glass' | 'glass-dark' | 'solid' | 'outline';
|
||||
size?: 'small' | 'medium' | 'large' | 'full';
|
||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
animated?: boolean;
|
||||
hover?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = 'glass',
|
||||
size = 'medium',
|
||||
rounded = 'xl',
|
||||
padding = 'md',
|
||||
shadow = 'md',
|
||||
animated = true,
|
||||
hover = false,
|
||||
className = ''
|
||||
} = Astro.props;
|
||||
|
||||
const variantClasses = {
|
||||
glass: 'container-glass',
|
||||
'glass-dark': 'container-glass-dark',
|
||||
solid: 'container-solid',
|
||||
outline: 'container-outline'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'container-small',
|
||||
medium: 'container-medium',
|
||||
large: 'container-large',
|
||||
full: 'container-full'
|
||||
};
|
||||
|
||||
const roundedClasses = {
|
||||
none: 'rounded-none',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
'2xl': 'rounded-2xl',
|
||||
'3xl': 'rounded-3xl',
|
||||
full: 'rounded-full'
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
none: 'p-0',
|
||||
sm: 'p-3',
|
||||
md: 'p-4 md:p-6',
|
||||
lg: 'p-6 md:p-8',
|
||||
xl: 'p-8 md:p-10',
|
||||
'2xl': 'p-10 md:p-12'
|
||||
};
|
||||
|
||||
const shadowClasses = {
|
||||
none: 'shadow-none',
|
||||
sm: 'shadow-sm',
|
||||
md: 'shadow-md',
|
||||
lg: 'shadow-lg',
|
||||
xl: 'shadow-xl',
|
||||
'2xl': 'shadow-2xl'
|
||||
};
|
||||
---
|
||||
|
||||
<div
|
||||
class={`
|
||||
container-base
|
||||
${variantClasses[variant]}
|
||||
${sizeClasses[size]}
|
||||
${roundedClasses[rounded]}
|
||||
${paddingClasses[padding]}
|
||||
${shadowClasses[shadow]}
|
||||
${animated ? 'container-animated' : ''}
|
||||
${hover ? 'container-hover' : ''}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container-base {
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 变体样式 */
|
||||
.container-glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.container-glass-dark {
|
||||
background: rgba(122, 107, 93, 0.15);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(166, 139, 123, 0.25);
|
||||
}
|
||||
|
||||
.container-solid {
|
||||
background: #F4F1E8;
|
||||
border: 1px solid #E8DCC0;
|
||||
}
|
||||
|
||||
.container-outline {
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 尺寸样式 */
|
||||
.container-small {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-medium {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-large {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.container-animated {
|
||||
animation: containerFadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.container-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.container-glass.container-hover:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.container-glass-dark.container-hover:hover {
|
||||
background: rgba(122, 107, 93, 0.25);
|
||||
border-color: rgba(166, 139, 123, 0.35);
|
||||
}
|
||||
|
||||
.container-outline.container-hover:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.container-small,
|
||||
.container-medium,
|
||||
.container-large {
|
||||
max-width: 100%;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes containerFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 实用工具类 */
|
||||
.p-0 { padding: 0 !important; }
|
||||
.p-3 { padding: 0.75rem !important; }
|
||||
.p-4 { padding: 1rem !important; }
|
||||
.p-6 { padding: 1.5rem !important; }
|
||||
.p-8 { padding: 2rem !important; }
|
||||
.p-10 { padding: 2.5rem !important; }
|
||||
.p-12 { padding: 3rem !important; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:p-6 { padding: 1.5rem !important; }
|
||||
.md\:p-8 { padding: 2rem !important; }
|
||||
.md\:p-10 { padding: 2.5rem !important; }
|
||||
.md\:p-12 { padding: 3rem !important; }
|
||||
}
|
||||
|
||||
.rounded-none { border-radius: 0 !important; }
|
||||
.rounded-sm { border-radius: 0.125rem !important; }
|
||||
.rounded-md { border-radius: 0.375rem !important; }
|
||||
.rounded-lg { border-radius: 0.5rem !important; }
|
||||
.rounded-xl { border-radius: 0.75rem !important; }
|
||||
.rounded-2xl { border-radius: 1rem !important; }
|
||||
.rounded-3xl { border-radius: 1.5rem !important; }
|
||||
.rounded-full { border-radius: 9999px !important; }
|
||||
|
||||
.shadow-none { box-shadow: none !important; }
|
||||
.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important; }
|
||||
.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important; }
|
||||
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important; }
|
||||
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1) !important; }
|
||||
.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important; }
|
||||
</style>
|
||||
211
src/components/Footer.astro
Normal file
211
src/components/Footer.astro
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
export interface Props {
|
||||
showSocialLinks?: boolean;
|
||||
customLinks?: Array<{ label: string; href: string; icon?: string }>;
|
||||
}
|
||||
|
||||
const {
|
||||
showSocialLinks = true,
|
||||
customLinks = []
|
||||
} = Astro.props;
|
||||
|
||||
const socialLinks = [
|
||||
{ label: "Email", href: "mailto:jiaotiansheng@outlook.com", icon: "fas fa-envelope" }
|
||||
];
|
||||
---
|
||||
|
||||
<footer class="footer-container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-main">
|
||||
<div class="footer-brand">
|
||||
<h3 class="brand-title">Jiao77</h3>
|
||||
<p class="brand-description">简约美观的现代化网站体验</p>
|
||||
</div>
|
||||
|
||||
{customLinks.length > 0 && (
|
||||
<div class="footer-links">
|
||||
<h4 class="links-title">快速链接</h4>
|
||||
<ul class="links-list">
|
||||
{customLinks.map(link => (
|
||||
<li>
|
||||
<a href={link.href} class="footer-link">
|
||||
{link.icon && <i class={link.icon}></i>}
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSocialLinks && (
|
||||
<div class="footer-social">
|
||||
<h4 class="social-title">联系方式</h4>
|
||||
<div class="social-links">
|
||||
{socialLinks.map(social => (
|
||||
<a href={social.href} class="social-link" title={social.label}>
|
||||
<i class={social.icon}></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="footer-divider"></div>
|
||||
<div class="footer-info">
|
||||
<p class="copyright">© 2024 Jiao77. All rights reserved.</p>
|
||||
<p class="built-with">Built with <i class="fas fa-heart text-red-400"></i> and Astro</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 2rem;
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer-brand .brand-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #011a2d;
|
||||
margin: 0 0 0.5rem 0;
|
||||
background: linear-gradient(135deg, #011a2d, #2c4a6b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.footer-brand .brand-description {
|
||||
color: #2c4a6b;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.links-title,
|
||||
.social-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #011a2d;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.links-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.links-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #2c4a6b;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #011a2d;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.75rem;
|
||||
color: #2c4a6b;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #011a2d;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.2) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: #2c4a6b;
|
||||
}
|
||||
|
||||
.copyright,
|
||||
.built-with {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.built-with i {
|
||||
color: #e74c3c;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-main {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
</style>
|
||||
198
src/components/Header.astro
Normal file
198
src/components/Header.astro
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
export interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
navigationItems?: Array<{ label: string; href: string; icon?: string }>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Jiao77 - AI & Technology Explorer'
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<header class="header-container">
|
||||
<!-- 收缩状态的页眉 -->
|
||||
<div class="header-collapsed" id="header-collapsed">
|
||||
<div class="header-content">
|
||||
<div class="header-brand">
|
||||
<h1 class="brand-title">{title}</h1>
|
||||
</div>
|
||||
|
||||
<button class="expand-button" id="expand-button">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开状态的页眉 -->
|
||||
<div class="header-expanded" id="header-expanded">
|
||||
<div class="header-content">
|
||||
<div class="header-brand">
|
||||
<h1 class="brand-title">{title}</h1>
|
||||
</div>
|
||||
|
||||
<nav class="main-nav" aria-label="主导航">
|
||||
<a href="/" class="nav-item" aria-label="首页">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>首页</span>
|
||||
</a>
|
||||
<a href="/reports" class="nav-item" aria-label="报告">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>报告</span>
|
||||
</a>
|
||||
<a href="/components-demo" class="nav-item" aria-label="组件示例">
|
||||
<i class="fas fa-cube"></i>
|
||||
<span>组件</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<button class="collapse-button" id="collapse-button">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.header-collapsed {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 1rem 0;
|
||||
transform: translateY(0);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-expanded {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(15px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 1.5rem 0;
|
||||
transform: translateY(-100%);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-expanded.active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.header-collapsed.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #011a2d;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #011a2d, #2c4a6b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #011a2d;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.expand-button,
|
||||
.collapse-button {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
color: #011a2d;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1.1rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expand-button:hover,
|
||||
.collapse-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navigation {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import '../scripts/header.ts';
|
||||
</script>
|
||||
320
src/components/common/CodeBlock.astro
Normal file
320
src/components/common/CodeBlock.astro
Normal file
@@ -0,0 +1,320 @@
|
||||
---
|
||||
export interface Props {
|
||||
code: string;
|
||||
language?: string;
|
||||
title?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
code,
|
||||
language = 'javascript',
|
||||
title,
|
||||
showLineNumbers = false,
|
||||
className = '',
|
||||
copyable = true
|
||||
} = Astro.props;
|
||||
|
||||
// 生成唯一ID
|
||||
const codeId = `code-${Math.random().toString(36).substring(2, 11)}`;
|
||||
---
|
||||
|
||||
<div class={`glass-code-block ${className}`}>
|
||||
{title && (
|
||||
<div class="code-header">
|
||||
<span class="code-title">{title}</span>
|
||||
<span class="code-language">{language}</span>
|
||||
{copyable && (
|
||||
<button class="copy-btn" data-code-id={codeId} title="复制代码">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div class="code-content">
|
||||
<pre class={`language-${language} ${showLineNumbers ? 'line-numbers' : ''}`}><code id={codeId} class={`language-${language}`}>{code}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 动态加载 Prism.js
|
||||
function loadPrism() {
|
||||
if ((window as any).Prism) return Promise.resolve();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// 加载 CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css';
|
||||
document.head.appendChild(cssLink);
|
||||
|
||||
// 加载行号插件 CSS
|
||||
const lineNumbersCss = document.createElement('link');
|
||||
lineNumbersCss.rel = 'stylesheet';
|
||||
lineNumbersCss.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css';
|
||||
document.head.appendChild(lineNumbersCss);
|
||||
|
||||
// 加载核心 JS
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js';
|
||||
script.onload = () => {
|
||||
// 加载自动加载插件
|
||||
const autoloaderScript = document.createElement('script');
|
||||
autoloaderScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js';
|
||||
autoloaderScript.onload = () => {
|
||||
// 加载行号插件
|
||||
const lineNumbersScript = document.createElement('script');
|
||||
lineNumbersScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js';
|
||||
lineNumbersScript.onload = () => resolve();
|
||||
lineNumbersScript.onerror = reject;
|
||||
document.head.appendChild(lineNumbersScript);
|
||||
};
|
||||
autoloaderScript.onerror = reject;
|
||||
document.head.appendChild(autoloaderScript);
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// 高亮代码
|
||||
function highlightCode() {
|
||||
loadPrism().then(() => {
|
||||
const prism = (window as any).Prism;
|
||||
if (prism && prism.highlightAll) {
|
||||
prism.highlightAll();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn('Failed to load Prism.js:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 复制代码功能
|
||||
function initCopyButtons() {
|
||||
document.querySelectorAll('.copy-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function(this: HTMLElement) {
|
||||
const codeId = this.getAttribute('data-code-id');
|
||||
const codeElement = document.getElementById(codeId || '');
|
||||
if (!codeElement) return;
|
||||
|
||||
const code = codeElement.textContent || '';
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
const icon = this.querySelector('i') as HTMLElement;
|
||||
const originalClass = icon.className;
|
||||
icon.className = 'fas fa-check';
|
||||
this.style.color = '#22c55e';
|
||||
|
||||
setTimeout(() => {
|
||||
icon.className = originalClass;
|
||||
this.style.color = '';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.warn('Failed to copy code:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
highlightCode();
|
||||
initCopyButtons();
|
||||
});
|
||||
} else {
|
||||
highlightCode();
|
||||
initCopyButtons();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.glass-code-block {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Maple Mono NF CN', 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.glass-code-block:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.code-title {
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.code-language {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: rgba(100, 116, 139, 0.1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #3b82f6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.code-content {
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.code-content pre {
|
||||
margin: 0;
|
||||
padding: 1.5rem 1.5rem 1.5rem 3.5rem;
|
||||
background: transparent !important;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
background: transparent !important;
|
||||
color: #334155 !important;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 覆盖 Prism 默认样式 */
|
||||
.code-content pre[class*="language-"] {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 行号样式 */
|
||||
.line-numbers .line-numbers-rows {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.2) !important;
|
||||
background: rgba(0, 0, 0, 0.05) !important;
|
||||
padding-left: 2rem !important;
|
||||
}
|
||||
|
||||
.line-numbers-rows > span:before {
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
/* 语法高亮颜色调整 */
|
||||
.code-content .token.comment,
|
||||
.code-content .token.prolog,
|
||||
.code-content .token.doctype,
|
||||
.code-content .token.cdata {
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
.code-content .token.punctuation {
|
||||
color: #475569 !important;
|
||||
}
|
||||
|
||||
.code-content .token.property,
|
||||
.code-content .token.tag,
|
||||
.code-content .token.boolean,
|
||||
.code-content .token.number,
|
||||
.code-content .token.constant,
|
||||
.code-content .token.symbol,
|
||||
.code-content .token.deleted {
|
||||
color: #d97706 !important;
|
||||
}
|
||||
|
||||
.code-content .token.selector,
|
||||
.code-content .token.attr-name,
|
||||
.code-content .token.string,
|
||||
.code-content .token.char,
|
||||
.code-content .token.builtin,
|
||||
.code-content .token.inserted {
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
.code-content .token.operator,
|
||||
.code-content .token.entity,
|
||||
.code-content .token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #2563eb !important;
|
||||
}
|
||||
|
||||
.code-content .token.atrule,
|
||||
.code-content .token.attr-value,
|
||||
.code-content .token.keyword {
|
||||
color: #7c3aed !important;
|
||||
}
|
||||
|
||||
.code-content .token.function,
|
||||
.code-content .token.class-name {
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.code-header {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.code-content pre {
|
||||
padding: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 0.375rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.code-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
166
src/components/common/GlassTable.astro
Normal file
166
src/components/common/GlassTable.astro
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
export interface Props {
|
||||
headers?: string[];
|
||||
rows?: (string | number | boolean)[][];
|
||||
caption?: string;
|
||||
className?: string;
|
||||
striped?: boolean;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
headers = [],
|
||||
rows = [],
|
||||
caption,
|
||||
className = '',
|
||||
striped = false,
|
||||
bordered = true
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`glass-table-container ${className}`}>
|
||||
<table class={`glass-table ${striped ? 'striped' : ''} ${bordered ? 'bordered' : ''}`}>
|
||||
{caption && (
|
||||
<caption class="table-caption">{caption}</caption>
|
||||
)}
|
||||
{headers.length > 0 && (
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map(header => (
|
||||
<th>{header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr>
|
||||
{row.map(cell => (
|
||||
<td>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glass-table-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-table-container:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.glass-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.table-caption {
|
||||
caption-side: top;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.glass-table th,
|
||||
.glass-table td {
|
||||
padding: 0.875rem 1rem;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.glass-table th {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #1e40af;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 2px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.glass-table td {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.glass-table.bordered th,
|
||||
.glass-table.bordered td {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.glass-table.bordered th:last-child,
|
||||
.glass-table.bordered td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.glass-table.striped tbody tr:nth-child(odd) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.glass-table tbody tr:hover {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.glass-table tbody tr:hover td {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.glass-table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.glass-table {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.glass-table th,
|
||||
.glass-table td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 特殊单元格样式 */
|
||||
.glass-table .highlight-cell {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #166534;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.glass-table .warning-cell {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #92400e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.glass-table .error-cell {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #991b1b;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
105
src/components/common/MathFormula.astro
Normal file
105
src/components/common/MathFormula.astro
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
export interface Props {
|
||||
formula: string;
|
||||
display?: boolean; // true for block display, false for inline
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const { formula, display = false, className = '' } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`math-formula ${display ? 'display-math' : 'inline-math'} ${className}`}>
|
||||
<span class="katex-formula" data-formula={formula}>{formula}</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 动态加载 KaTeX
|
||||
function loadKaTeX() {
|
||||
if ((window as any).katex) return Promise.resolve();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js';
|
||||
script.onload = () => {
|
||||
const autoRenderScript = document.createElement('script');
|
||||
autoRenderScript.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js';
|
||||
autoRenderScript.onload = () => resolve();
|
||||
autoRenderScript.onerror = reject;
|
||||
document.head.appendChild(autoRenderScript);
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染数学公式
|
||||
function renderMath() {
|
||||
loadKaTeX().then(() => {
|
||||
const formulas = document.querySelectorAll('.katex-formula');
|
||||
formulas.forEach((formula) => {
|
||||
const text = formula.getAttribute('data-formula') || formula.textContent;
|
||||
const isDisplay = formula.closest('.display-math');
|
||||
|
||||
try {
|
||||
(window as any).katex.render(text, formula, {
|
||||
throwOnError: false,
|
||||
displayMode: !!isDisplay
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('KaTeX rendering error:', error);
|
||||
(formula as HTMLElement).textContent = text || '';
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.warn('Failed to load KaTeX:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后渲染
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', renderMath);
|
||||
} else {
|
||||
renderMath();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.math-formula {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.inline-math {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.display-math {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.math-formula:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.katex-formula {
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
227
src/components/navigation/NavigationCard.astro
Normal file
227
src/components/navigation/NavigationCard.astro
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
href: string;
|
||||
icon?: string;
|
||||
color?: 'primary' | 'secondary' | 'accent';
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
icon,
|
||||
color = 'primary',
|
||||
size = 'medium'
|
||||
} = Astro.props;
|
||||
|
||||
// 判断是否为外部链接
|
||||
const isExternalLink = href.startsWith('http://') || href.startsWith('https://');
|
||||
const linkTarget = isExternalLink ? '_blank' : undefined;
|
||||
const linkRel = isExternalLink ? 'noopener noreferrer' : undefined;
|
||||
|
||||
const colorClasses = {
|
||||
primary: 'nav-card-primary',
|
||||
secondary: 'nav-card-secondary',
|
||||
accent: 'nav-card-accent'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'nav-card-small',
|
||||
medium: 'nav-card-medium',
|
||||
large: 'nav-card-large'
|
||||
};
|
||||
---
|
||||
|
||||
<a href={href} target={linkTarget} rel={linkRel} class={`nav-card ${colorClasses[color]} ${sizeClasses[size]}`}>
|
||||
<div class="nav-card-content">
|
||||
{icon && (
|
||||
<div class="nav-card-icon">
|
||||
<i class={icon}></i>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 class="nav-card-title">{title}</h3>
|
||||
|
||||
{description && (
|
||||
<p class="nav-card-description">{description}</p>
|
||||
)}
|
||||
|
||||
<div class="nav-card-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-card-glow"></div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.nav-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.nav-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.nav-card-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-card-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #5b778e;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card:hover .nav-card-icon {
|
||||
color: #011a2d;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.nav-card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #011a2d;
|
||||
margin: 0 0 0.5rem 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card-description {
|
||||
font-size: 0.875rem;
|
||||
color: #2c4a6b;
|
||||
margin: 0 0 1rem 0;
|
||||
line-height: 1.5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.nav-card-arrow {
|
||||
font-size: 1rem;
|
||||
color: #5b778e;
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card:hover .nav-card-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.nav-card-glow {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-card:hover .nav-card-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 尺寸变体 */
|
||||
.nav-card-small {
|
||||
min-height: 150px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-card-small .nav-card-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.nav-card-small .nav-card-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.nav-card-medium {
|
||||
min-height: 200px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.nav-card-large {
|
||||
min-height: 280px;
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.nav-card-large .nav-card-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.nav-card-large .nav-card-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* 颜色变体 */
|
||||
.nav-card-primary {
|
||||
background: rgba(91, 119, 142, 0.1);
|
||||
border-color: rgba(91, 119, 142, 0.2);
|
||||
}
|
||||
|
||||
.nav-card-primary:hover {
|
||||
background: rgba(91, 119, 142, 0.15);
|
||||
border-color: rgba(91, 119, 142, 0.3);
|
||||
}
|
||||
|
||||
.nav-card-secondary {
|
||||
background: rgba(178, 197, 213, 0.1);
|
||||
border-color: rgba(178, 197, 213, 0.2);
|
||||
}
|
||||
|
||||
.nav-card-secondary:hover {
|
||||
background: rgba(178, 197, 213, 0.15);
|
||||
border-color: rgba(178, 197, 213, 0.3);
|
||||
}
|
||||
|
||||
.nav-card-accent {
|
||||
background: rgba(177, 217, 212, 0.1);
|
||||
border-color: rgba(177, 217, 212, 0.2);
|
||||
}
|
||||
|
||||
.nav-card-accent:hover {
|
||||
background: rgba(177, 217, 212, 0.15);
|
||||
border-color: rgba(177, 217, 212, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-card {
|
||||
padding: 1.5rem;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.nav-card-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.nav-card-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
src/components/navigation/NavigationGrid.astro
Normal file
136
src/components/navigation/NavigationGrid.astro
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
export interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
columns?: number;
|
||||
gap?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
columns = 3,
|
||||
gap = 'medium'
|
||||
} = Astro.props;
|
||||
|
||||
const gapClasses = {
|
||||
small: 'gap-4',
|
||||
medium: 'gap-6',
|
||||
large: 'gap-8'
|
||||
};
|
||||
---
|
||||
|
||||
<section class="navigation-grid-container">
|
||||
{(title || description) && (
|
||||
<div class="grid-header">
|
||||
{title && <h2 class="grid-title">{title}</h2>}
|
||||
{description && <p class="grid-description">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class={`navigation-grid ${gapClasses[gap]}`} style={`--columns: ${columns}`}>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.navigation-grid-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.grid-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #7A6B5D;
|
||||
margin: 0 0 1rem 0;
|
||||
background: linear-gradient(135deg, #7A6B5D, #A68B7B);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.grid-description {
|
||||
font-size: 1.1rem;
|
||||
color: #9B8B7A;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式列数 */
|
||||
@media (min-width: 1200px) {
|
||||
.navigation-grid {
|
||||
grid-template-columns: repeat(var(--columns, 3), 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navigation-grid-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.grid-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grid-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.navigation-grid-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 交错动画 */
|
||||
.navigation-grid > * {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.navigation-grid > *:nth-child(1) { animation-delay: 0.1s; }
|
||||
.navigation-grid > *:nth-child(2) { animation-delay: 0.2s; }
|
||||
.navigation-grid > *:nth-child(3) { animation-delay: 0.3s; }
|
||||
.navigation-grid > *:nth-child(4) { animation-delay: 0.4s; }
|
||||
.navigation-grid > *:nth-child(5) { animation-delay: 0.5s; }
|
||||
.navigation-grid > *:nth-child(6) { animation-delay: 0.6s; }
|
||||
.navigation-grid > *:nth-child(n+7) { animation-delay: 0.7s; }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
218
src/components/report/MetricCard.astro
Normal file
218
src/components/report/MetricCard.astro
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
value: string | number;
|
||||
change?: string;
|
||||
changeType?: 'positive' | 'negative' | 'neutral';
|
||||
icon?: string;
|
||||
color?: 'primary' | 'success' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
changeType = 'neutral',
|
||||
icon,
|
||||
color = 'primary'
|
||||
} = Astro.props;
|
||||
|
||||
const colorClasses = {
|
||||
primary: 'metric-primary',
|
||||
success: 'metric-success',
|
||||
warning: 'metric-warning',
|
||||
info: 'metric-info'
|
||||
};
|
||||
|
||||
const changeClasses = {
|
||||
positive: 'change-positive',
|
||||
negative: 'change-negative',
|
||||
neutral: 'change-neutral'
|
||||
};
|
||||
---
|
||||
|
||||
<div class={`metric-card ${colorClasses[color]}`}>
|
||||
<div class="metric-header">
|
||||
{icon && (
|
||||
<div class="metric-icon">
|
||||
<i class={icon}></i>
|
||||
</div>
|
||||
)}
|
||||
<h3 class="metric-title">{title}</h3>
|
||||
</div>
|
||||
|
||||
<div class="metric-body">
|
||||
<div class="metric-value">{value}</div>
|
||||
|
||||
{change && (
|
||||
<div class={`metric-change ${changeClasses[changeType]}`}>
|
||||
<i class={changeType === 'positive' ? 'fas fa-arrow-up' : changeType === 'negative' ? 'fas fa-arrow-down' : 'fas fa-minus'}></i>
|
||||
<span>{change}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="metric-bg"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.metric-card {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(91, 119, 142, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: #5b778e;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #011a2d;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.metric-body {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #011a2d;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.change-positive {
|
||||
color: #064630;
|
||||
}
|
||||
|
||||
.change-negative {
|
||||
color: #720808;
|
||||
}
|
||||
|
||||
.change-neutral {
|
||||
color: #0a2c48;
|
||||
}
|
||||
|
||||
.metric-bg {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
opacity: 0.03;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover .metric-bg {
|
||||
opacity: 0.06;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 颜色变体 */
|
||||
.metric-primary .metric-bg {
|
||||
background: linear-gradient(135deg, #5b778e, #b2c5d5);
|
||||
}
|
||||
|
||||
.metric-primary .metric-icon {
|
||||
background: rgba(91, 119, 142, 0.2);
|
||||
color: #5b778e;
|
||||
}
|
||||
|
||||
.metric-success .metric-bg {
|
||||
background: linear-gradient(135deg, #b1d9d4, #aecedd);
|
||||
}
|
||||
|
||||
.metric-success .metric-icon {
|
||||
background: rgba(177, 217, 212, 0.2);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.metric-warning .metric-bg {
|
||||
background: linear-gradient(135deg, #b2c5d5, #aecedd);
|
||||
}
|
||||
|
||||
.metric-warning .metric-icon {
|
||||
background: rgba(178, 197, 213, 0.2);
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
.metric-info .metric-bg {
|
||||
background: linear-gradient(135deg, #aecedd, #b1d9d4);
|
||||
}
|
||||
|
||||
.metric-info .metric-icon {
|
||||
background: rgba(174, 206, 221, 0.2);
|
||||
color: #5b778e;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metric-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
93
src/components/report/MetricsGrid.astro
Normal file
93
src/components/report/MetricsGrid.astro
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
export interface Props {
|
||||
title?: string;
|
||||
columns?: number;
|
||||
gap?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
columns = 4,
|
||||
gap = 'medium'
|
||||
} = Astro.props;
|
||||
|
||||
const gapClasses = {
|
||||
small: 'gap-4',
|
||||
medium: 'gap-6',
|
||||
large: 'gap-8'
|
||||
};
|
||||
---
|
||||
|
||||
<div class="metrics-grid-container">
|
||||
{title && (
|
||||
<h3 class="metrics-title">{title}</h3>
|
||||
)}
|
||||
|
||||
<div class={`metrics-grid ${gapClasses[gap]}`} style={`--columns: ${columns}`}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.metrics-grid-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.metrics-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #011a2d;
|
||||
margin: 0 0 1.5rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(91, 119, 142, 0.3);
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式列数 */
|
||||
@media (min-width: 1200px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(var(--columns, 4), 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 交错动画 */
|
||||
.metrics-grid > * {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.metrics-grid > *:nth-child(1) { animation-delay: 0.1s; }
|
||||
.metrics-grid > *:nth-child(2) { animation-delay: 0.2s; }
|
||||
.metrics-grid > *:nth-child(3) { animation-delay: 0.3s; }
|
||||
.metrics-grid > *:nth-child(4) { animation-delay: 0.4s; }
|
||||
.metrics-grid > *:nth-child(n+5) { animation-delay: 0.5s; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
177
src/components/report/ReportSection.astro
Normal file
177
src/components/report/ReportSection.astro
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
export interface Props {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
level?: 1 | 2 | 3 | 4;
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
level = 1,
|
||||
divider = true
|
||||
} = Astro.props;
|
||||
|
||||
const Tag = `h${level}` as any;
|
||||
const sizeClasses = {
|
||||
1: 'text-3xl md:text-4xl',
|
||||
2: 'text-2xl md:text-3xl',
|
||||
3: 'text-xl md:text-2xl',
|
||||
4: 'text-lg md:text-xl'
|
||||
};
|
||||
---
|
||||
|
||||
<section class="report-section-container">
|
||||
<div class="section-header">
|
||||
{title && (
|
||||
<Tag class={`section-title ${sizeClasses[level]}`}>
|
||||
{title}
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
{subtitle && (
|
||||
<p class="section-subtitle">{subtitle}</p>
|
||||
)}
|
||||
|
||||
{divider && <div class="section-divider"></div>}
|
||||
</div>
|
||||
|
||||
<div class="section-content">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.report-section-container {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.report-section-container:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
color: #011a2d;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.2;
|
||||
background: linear-gradient(135deg, #011a2d, #2c4a6b, #5b778e);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 1rem;
|
||||
color: #011a2d;
|
||||
margin: 0 0 1rem 0;
|
||||
line-height: 1.5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(91, 119, 142, 0.3) 20%,
|
||||
rgba(91, 119, 142, 0.6) 50%,
|
||||
rgba(91, 119, 142, 0.3) 80%,
|
||||
transparent 100%
|
||||
);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
color: #011a2d;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 主题配色下的内容样式 */
|
||||
.section-content :global(p) {
|
||||
margin-bottom: 1rem;
|
||||
color: #011a2d;
|
||||
}
|
||||
|
||||
.section-content :global(ul),
|
||||
.section-content :global(ol) {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.section-content :global(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #011a2d;
|
||||
}
|
||||
|
||||
.section-content :global(strong) {
|
||||
color: #011a2d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-content :global(code) {
|
||||
background: rgba(91, 119, 142, 0.15);
|
||||
color: #011a2d;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.section-content :global(pre) {
|
||||
background: rgba(91, 119, 142, 0.1);
|
||||
border: 1px solid rgba(91, 119, 142, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.section-content :global(blockquote) {
|
||||
border-left: 4px solid #5b778e;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
color: #011a2d;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.report-section-container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user