display
This commit is contained in:
parent
3a014a3d20
commit
12708e6149
434
meteor-frontend/meteor_homepage_design.html
Normal file
434
meteor-frontend/meteor_homepage_design.html
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>分布式流星监测网络</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #0a0a23 0%, #1a1a3a 50%, #2a2a4a 100%);
|
||||||
|
color: white;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.starfield {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: twinkle 2s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0% { opacity: 0.3; transform: scale(1); }
|
||||||
|
100% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor {
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 100px;
|
||||||
|
background: linear-gradient(to bottom, #ff6b35, transparent);
|
||||||
|
animation: meteor-fall 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes meteor-fall {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100px) translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh) translateX(-200px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(45deg, #ffffff, #ff6b35);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0% { text-shadow: 0 0 20px rgba(255, 107, 53, 0.5); }
|
||||||
|
100% { text-shadow: 0 0 40px rgba(255, 107, 53, 0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.science-visual {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.constellation {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
position: relative;
|
||||||
|
background: radial-gradient(circle at center, rgba(255, 107, 53, 0.1), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.constellation-dot {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #ff6b35;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 15px #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constellation-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, #ff6b35, transparent);
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-prop {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-prop h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-prop p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 40px rgba(255, 107, 53, 0.2);
|
||||||
|
border-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: linear-gradient(45deg, #ff6b35, #ff8c42);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: #cccccc;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(45deg, #ff6b35, #ff8c42);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 30px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: #ff6b35;
|
||||||
|
border: 2px solid #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #ff6b35;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="starfield" id="starfield"></div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="hero">
|
||||||
|
<h1 class="hero-title">分布式流星监测网络</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
连接全球观测者,记录天空中每一道流星的轨迹,为科学研究提供珍贵数据
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="split-section">
|
||||||
|
<div class="science-visual">
|
||||||
|
<div class="constellation" id="constellation">
|
||||||
|
<!-- 星座连线将通过JavaScript生成 -->
|
||||||
|
</div>
|
||||||
|
<h3 style="text-align: center; margin-top: 1rem; color: #ff6b35;">实时监测网络</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-prop">
|
||||||
|
<h2>科学价值</h2>
|
||||||
|
<p>
|
||||||
|
通过分布式监测网络,我们能够精确追踪流星轨迹,分析其起源、速度和成分。
|
||||||
|
每一次观测都为天体物理学研究贡献宝贵数据。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
加入我们的网络,成为公民科学家,让您的观测为人类对宇宙的理解做出贡献。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="feature-cards">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🛰️</div>
|
||||||
|
<h3>设备网络</h3>
|
||||||
|
<p>分布在全球的高精度监测设备,24小时不间断捕捉流星事件,构建完整的观测网络。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📸</div>
|
||||||
|
<h3>事件画廊</h3>
|
||||||
|
<p>精彩的流星照片和视频集合,每一帧都记录着宇宙的壮丽瞬间,见证天空的奇迹。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📊</div>
|
||||||
|
<h3>研究数据</h3>
|
||||||
|
<p>开放的科学数据平台,为天文学家和研究人员提供准确、详细的流星观测数据。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cta-section">
|
||||||
|
<h2 style="font-size: 2.5rem; margin-bottom: 1.5rem; color: #ff6b35;">
|
||||||
|
开始您的星空探索之旅
|
||||||
|
</h2>
|
||||||
|
<p style="font-size: 1.2rem; color: #cccccc; margin-bottom: 2rem;">
|
||||||
|
注册账户,访问实时数据,加入全球观测者社区
|
||||||
|
</p>
|
||||||
|
<div class="cta-buttons">
|
||||||
|
<a href="#" class="btn btn-primary">立即注册</a>
|
||||||
|
<a href="#" class="btn btn-secondary">了解更多</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 生成星空背景
|
||||||
|
function createStarfield() {
|
||||||
|
const starfield = document.getElementById('starfield');
|
||||||
|
const numStars = 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < numStars; i++) {
|
||||||
|
const star = document.createElement('div');
|
||||||
|
star.className = 'star';
|
||||||
|
star.style.left = Math.random() * 100 + '%';
|
||||||
|
star.style.top = Math.random() * 100 + '%';
|
||||||
|
star.style.width = Math.random() * 3 + 1 + 'px';
|
||||||
|
star.style.height = star.style.width;
|
||||||
|
star.style.animationDelay = Math.random() * 2 + 's';
|
||||||
|
starfield.appendChild(star);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加流星
|
||||||
|
setInterval(() => {
|
||||||
|
const meteor = document.createElement('div');
|
||||||
|
meteor.className = 'meteor';
|
||||||
|
meteor.style.left = Math.random() * 100 + '%';
|
||||||
|
meteor.style.animationDuration = (Math.random() * 2 + 2) + 's';
|
||||||
|
starfield.appendChild(meteor);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
meteor.remove();
|
||||||
|
}, 5000);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成星座图案
|
||||||
|
function createConstellation() {
|
||||||
|
const constellation = document.getElementById('constellation');
|
||||||
|
const dots = [
|
||||||
|
{x: 20, y: 30}, {x: 40, y: 20}, {x: 60, y: 40},
|
||||||
|
{x: 30, y: 60}, {x: 70, y: 50}, {x: 50, y: 80},
|
||||||
|
{x: 80, y: 70}, {x: 15, y: 75}, {x: 85, y: 25}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 创建星点
|
||||||
|
dots.forEach((dot, index) => {
|
||||||
|
const dotEl = document.createElement('div');
|
||||||
|
dotEl.className = 'constellation-dot';
|
||||||
|
dotEl.style.left = dot.x + '%';
|
||||||
|
dotEl.style.top = dot.y + '%';
|
||||||
|
dotEl.style.animationDelay = index * 0.2 + 's';
|
||||||
|
constellation.appendChild(dotEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建连线
|
||||||
|
const connections = [
|
||||||
|
[0, 1], [1, 2], [2, 4], [4, 6], [3, 5], [5, 7], [0, 3]
|
||||||
|
];
|
||||||
|
|
||||||
|
connections.forEach(([start, end]) => {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'constellation-line';
|
||||||
|
|
||||||
|
const startDot = dots[start];
|
||||||
|
const endDot = dots[end];
|
||||||
|
const dx = endDot.x - startDot.x;
|
||||||
|
const dy = endDot.y - startDot.y;
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
||||||
|
|
||||||
|
line.style.left = startDot.x + '%';
|
||||||
|
line.style.top = startDot.y + '%';
|
||||||
|
line.style.width = length + '%';
|
||||||
|
line.style.transform = `rotate(${angle}deg)`;
|
||||||
|
|
||||||
|
constellation.appendChild(line);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平滑滚动动画
|
||||||
|
function handleScrollAnimations() {
|
||||||
|
const cards = document.querySelectorAll('.feature-card');
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.style.opacity = '1';
|
||||||
|
entry.target.style.transform = 'translateY(0)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.1 });
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(50px)';
|
||||||
|
card.style.transition = 'all 0.6s ease';
|
||||||
|
observer.observe(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
createStarfield();
|
||||||
|
createConstellation();
|
||||||
|
handleScrollAnimations();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,26 +1,26 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #0a0a23;
|
||||||
--foreground: #171717;
|
--foreground: #ffffff;
|
||||||
--card: #ffffff;
|
--card: rgba(255, 255, 255, 0.05);
|
||||||
--card-foreground: #171717;
|
--card-foreground: #ffffff;
|
||||||
--popover: #ffffff;
|
--popover: rgba(255, 255, 255, 0.05);
|
||||||
--popover-foreground: #171717;
|
--popover-foreground: #ffffff;
|
||||||
--primary: #171717;
|
--primary: #ff6b35;
|
||||||
--primary-foreground: #fafafa;
|
--primary-foreground: #ffffff;
|
||||||
--secondary: #f5f5f5;
|
--secondary: rgba(255, 255, 255, 0.1);
|
||||||
--secondary-foreground: #171717;
|
--secondary-foreground: #ffffff;
|
||||||
--muted: #f5f5f5;
|
--muted: rgba(255, 255, 255, 0.05);
|
||||||
--muted-foreground: #737373;
|
--muted-foreground: #cccccc;
|
||||||
--accent: #f5f5f5;
|
--accent: #ff6b35;
|
||||||
--accent-foreground: #171717;
|
--accent-foreground: #ffffff;
|
||||||
--destructive: #ef4444;
|
--destructive: #ef4444;
|
||||||
--destructive-foreground: #fafafa;
|
--destructive-foreground: #ffffff;
|
||||||
--border: #e5e5e5;
|
--border: rgba(255, 255, 255, 0.1);
|
||||||
--input: #e5e5e5;
|
--input: rgba(255, 255, 255, 0.1);
|
||||||
--ring: #171717;
|
--ring: #ff6b35;
|
||||||
--radius: 0.5rem;
|
--radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@ -50,30 +50,99 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a23;
|
||||||
--foreground: #ededed;
|
--foreground: #ffffff;
|
||||||
--card: #0a0a0a;
|
--card: rgba(255, 255, 255, 0.05);
|
||||||
--card-foreground: #ededed;
|
--card-foreground: #ffffff;
|
||||||
--popover: #0a0a0a;
|
--popover: rgba(255, 255, 255, 0.05);
|
||||||
--popover-foreground: #ededed;
|
--popover-foreground: #ffffff;
|
||||||
--primary: #ededed;
|
--primary: #ff6b35;
|
||||||
--primary-foreground: #0a0a0a;
|
--primary-foreground: #ffffff;
|
||||||
--secondary: #262626;
|
--secondary: rgba(255, 255, 255, 0.1);
|
||||||
--secondary-foreground: #ededed;
|
--secondary-foreground: #ffffff;
|
||||||
--muted: #262626;
|
--muted: rgba(255, 255, 255, 0.05);
|
||||||
--muted-foreground: #a3a3a3;
|
--muted-foreground: #cccccc;
|
||||||
--accent: #262626;
|
--accent: #ff6b35;
|
||||||
--accent-foreground: #ededed;
|
--accent-foreground: #ffffff;
|
||||||
--destructive: #dc2626;
|
--destructive: #ef4444;
|
||||||
--destructive-foreground: #ededed;
|
--destructive-foreground: #ffffff;
|
||||||
--border: #262626;
|
--border: rgba(255, 255, 255, 0.1);
|
||||||
--input: #262626;
|
--input: rgba(255, 255, 255, 0.1);
|
||||||
--ring: #d4d4d8;
|
--ring: #ff6b35;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: linear-gradient(135deg, #0a0a23 0%, #1a1a3a 50%, #2a2a4a 100%);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.starfield {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: twinkle 2s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0% { opacity: 0.3; transform: scale(1); }
|
||||||
|
100% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor {
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 100px;
|
||||||
|
background: linear-gradient(to bottom, #ff6b35, transparent);
|
||||||
|
animation: meteor-fall 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes meteor-fall {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100px) translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh) translateX(-200px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title-gradient {
|
||||||
|
background: linear-gradient(45deg, #ffffff, #ff6b35);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0% { text-shadow: 0 0 20px rgba(255, 107, 53, 0.5); }
|
||||||
|
100% { text-shadow: 0 0 40px rgba(255, 107, 53, 0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 40px rgba(255, 107, 53, 0.2);
|
||||||
|
border-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "分布式流星监测网络 - Distributed Meteor Monitoring Network",
|
||||||
description: "Generated by create next app",
|
description: "连接全球观测者,记录天空中每一道流星的轨迹,为科学研究提供珍贵数据",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -25,7 +25,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,18 +3,58 @@
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useAuth } from "@/contexts/auth-context"
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { isAuthenticated, user } = useAuth()
|
const { isAuthenticated, user } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createStarfield()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createStarfield = () => {
|
||||||
|
const starfield = document.getElementById('starfield')
|
||||||
|
if (!starfield) return
|
||||||
|
|
||||||
|
const numStars = 100
|
||||||
|
|
||||||
|
for (let i = 0; i < numStars; i++) {
|
||||||
|
const star = document.createElement('div')
|
||||||
|
star.className = 'star'
|
||||||
|
star.style.left = Math.random() * 100 + '%'
|
||||||
|
star.style.top = Math.random() * 100 + '%'
|
||||||
|
star.style.width = Math.random() * 3 + 1 + 'px'
|
||||||
|
star.style.height = star.style.width
|
||||||
|
star.style.animationDelay = Math.random() * 2 + 's'
|
||||||
|
starfield.appendChild(star)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add falling meteors
|
||||||
|
const createMeteor = () => {
|
||||||
|
const meteor = document.createElement('div')
|
||||||
|
meteor.className = 'meteor'
|
||||||
|
meteor.style.left = Math.random() * 100 + '%'
|
||||||
|
meteor.style.animationDuration = (Math.random() * 2 + 2) + 's'
|
||||||
|
starfield.appendChild(meteor)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
meteor.remove()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(createMeteor, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col relative">
|
||||||
|
<div id="starfield" className="starfield"></div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<header className="absolute top-0 right-0 p-6 z-10">
|
<header className="absolute top-0 right-0 p-6 z-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
<span className="text-sm text-gray-300">
|
||||||
Welcome, {user?.email}
|
Welcome, {user?.email}
|
||||||
</span>
|
</span>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
@ -34,28 +74,88 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Hero Section */}
|
||||||
<div className="flex-1 flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
|
<section className="min-h-screen flex flex-col justify-center items-center text-center px-4 relative">
|
||||||
<main className="text-center px-4">
|
<h1 className="text-6xl font-bold mb-4 hero-title-gradient">
|
||||||
<h1 className="text-6xl font-bold text-gray-900 dark:text-white mb-4">
|
分布式流星监测网络
|
||||||
分布式流星监测网络
|
</h1>
|
||||||
</h1>
|
<p className="text-xl text-gray-300 mb-8 max-w-2xl">
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
|
连接全球观测者,记录天空中每一道流星的轨迹,为科学研究提供珍贵数据
|
||||||
Distributed Meteor Monitoring Network
|
</p>
|
||||||
</p>
|
<p className="text-lg text-gray-400 mb-8">
|
||||||
|
Distributed Meteor Monitoring Network
|
||||||
{!isAuthenticated && (
|
</p>
|
||||||
<div className="flex items-center justify-center gap-4">
|
|
||||||
<Button size="lg" asChild>
|
{!isAuthenticated && (
|
||||||
<Link href="/register">Join the Network</Link>
|
<div className="flex items-center justify-center gap-4 flex-wrap">
|
||||||
|
<Button size="lg" asChild className="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700">
|
||||||
|
<Link href="/register">加入监测网络</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="lg" asChild className="border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white">
|
||||||
|
<Link href="/login">Sign In</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Feature Cards Section */}
|
||||||
|
<section className="py-16 px-4">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div className="glass-card p-8 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-r from-orange-500 to-orange-600 rounded-2xl mx-auto mb-6 flex items-center justify-center text-2xl">
|
||||||
|
🛰️
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-white">设备网络</h3>
|
||||||
|
<p className="text-gray-300 leading-relaxed">
|
||||||
|
分布在全球的高精度监测设备,24小时不间断捕捉流星事件,构建完整的观测网络。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-card p-8 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-r from-orange-500 to-orange-600 rounded-2xl mx-auto mb-6 flex items-center justify-center text-2xl">
|
||||||
|
📸
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-white">事件画廊</h3>
|
||||||
|
<p className="text-gray-300 leading-relaxed">
|
||||||
|
精彩的流星照片和视频集合,每一帧都记录着宇宙的壮丽瞬间,见证天空的奇迹。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-card p-8 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-r from-orange-500 to-orange-600 rounded-2xl mx-auto mb-6 flex items-center justify-center text-2xl">
|
||||||
|
📊
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-white">研究数据</h3>
|
||||||
|
<p className="text-gray-300 leading-relaxed">
|
||||||
|
开放的科学数据平台,为天文学家和研究人员提供准确、详细的流星观测数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
{!isAuthenticated && (
|
||||||
|
<section className="py-16 px-4 text-center">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h2 className="text-4xl font-bold mb-6 text-orange-500">
|
||||||
|
开始您的星空探索之旅
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-300 mb-8">
|
||||||
|
注册账户,访问实时数据,加入全球观测者社区
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-4 flex-wrap">
|
||||||
|
<Button size="lg" asChild className="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 px-8 py-3 text-lg">
|
||||||
|
<Link href="/register">立即注册</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="lg" asChild>
|
<Button variant="outline" size="lg" asChild className="border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white px-8 py-3 text-lg">
|
||||||
<Link href="/login">Sign In</Link>
|
<Link href="/gallery">了解更多</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</main>
|
</section>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
136
meteor-web-backend/scripts/create-premium-user.js
Normal file
136
meteor-web-backend/scripts/create-premium-user.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建付费测试用户的脚本
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node scripts/create-premium-user.js [email] [password] [displayName]
|
||||||
|
*
|
||||||
|
* 例子:
|
||||||
|
* node scripts/create-premium-user.js premium@test.com TestPass123 "Premium User"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||||
|
|
||||||
|
async function createPremiumUser(email, password, displayName) {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log('✅ Connected to database');
|
||||||
|
|
||||||
|
// 1. 首先注册普通用户
|
||||||
|
console.log(`\n📝 Registering user: ${email}`);
|
||||||
|
|
||||||
|
const registerResponse = await fetch('http://localhost:3001/api/v1/auth/register-email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
displayName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!registerResponse.ok) {
|
||||||
|
const error = await registerResponse.json();
|
||||||
|
throw new Error(`Registration failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerData = await registerResponse.json();
|
||||||
|
const userId = registerData.userId;
|
||||||
|
|
||||||
|
console.log(`✅ User registered successfully with ID: ${userId}`);
|
||||||
|
|
||||||
|
// 2. 更新用户为付费用户
|
||||||
|
const fakeCustomerId = 'cus_test_premium_' + Date.now();
|
||||||
|
const fakeSubscriptionId = 'sub_test_premium_' + Date.now();
|
||||||
|
|
||||||
|
const updateQuery = `
|
||||||
|
UPDATE user_profiles
|
||||||
|
SET
|
||||||
|
payment_provider_customer_id = $1,
|
||||||
|
payment_provider_subscription_id = $2,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $3
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await client.query(updateQuery, [fakeCustomerId, fakeSubscriptionId, userId]);
|
||||||
|
|
||||||
|
if (result.rowCount > 0) {
|
||||||
|
console.log('✅ User upgraded to premium successfully!');
|
||||||
|
|
||||||
|
// 3. 测试登录和profile获取
|
||||||
|
console.log('\n🔐 Testing login...');
|
||||||
|
const loginResponse = await fetch('http://localhost:3001/api/v1/auth/login-email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginResponse.ok) {
|
||||||
|
const loginData = await loginResponse.json();
|
||||||
|
console.log('✅ Login successful');
|
||||||
|
|
||||||
|
// 测试profile
|
||||||
|
const profileResponse = await fetch('http://localhost:3001/api/v1/auth/profile', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${loginData.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profileResponse.ok) {
|
||||||
|
const profileData = await profileResponse.json();
|
||||||
|
console.log('✅ Profile retrieved successfully');
|
||||||
|
|
||||||
|
console.log('\n🎉 Premium user created successfully!');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
console.log(`👤 User Details:`);
|
||||||
|
console.log(` Email: ${email}`);
|
||||||
|
console.log(` Password: ${password}`);
|
||||||
|
console.log(` Display Name: ${displayName}`);
|
||||||
|
console.log(` User ID: ${userId}`);
|
||||||
|
console.log(` Customer ID: ${fakeCustomerId}`);
|
||||||
|
console.log(` Subscription ID: ${fakeSubscriptionId}`);
|
||||||
|
console.log(` Subscription Status: ${profileData.subscriptionStatus}`);
|
||||||
|
console.log(` Has Active Subscription: ${profileData.hasActiveSubscription}`);
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ Failed to upgrade user to premium');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令行参数处理
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const email = args[0] || 'premium' + Date.now() + '@test.com';
|
||||||
|
const password = args[1] || 'TestPassword123';
|
||||||
|
const displayName = args[2] || 'Premium User';
|
||||||
|
|
||||||
|
console.log('🚀 Creating Premium User...');
|
||||||
|
console.log(`Email: ${email}`);
|
||||||
|
console.log(`Password: ${password}`);
|
||||||
|
console.log(`Display Name: ${displayName}`);
|
||||||
|
|
||||||
|
createPremiumUser(email, password, displayName);
|
||||||
30
test-api.js
Normal file
30
test-api.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
async function testAPI() {
|
||||||
|
try {
|
||||||
|
console.log('Testing register-email API...');
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:3001/api/v1/auth/register-email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'testuser@example.com',
|
||||||
|
password: 'TestPass123',
|
||||||
|
displayName: 'Test User'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response headers:', Object.fromEntries(response.headers));
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
console.log('Response body:', data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testAPI();
|
||||||
Loading…
x
Reference in New Issue
Block a user