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";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #171717;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #171717;
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #f5f5f5;
|
||||
--secondary-foreground: #171717;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
--accent: #f5f5f5;
|
||||
--accent-foreground: #171717;
|
||||
--background: #0a0a23;
|
||||
--foreground: #ffffff;
|
||||
--card: rgba(255, 255, 255, 0.05);
|
||||
--card-foreground: #ffffff;
|
||||
--popover: rgba(255, 255, 255, 0.05);
|
||||
--popover-foreground: #ffffff;
|
||||
--primary: #ff6b35;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: rgba(255, 255, 255, 0.1);
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: rgba(255, 255, 255, 0.05);
|
||||
--muted-foreground: #cccccc;
|
||||
--accent: #ff6b35;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #fafafa;
|
||||
--border: #e5e5e5;
|
||||
--input: #e5e5e5;
|
||||
--ring: #171717;
|
||||
--radius: 0.5rem;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--input: rgba(255, 255, 255, 0.1);
|
||||
--ring: #ff6b35;
|
||||
--radius: 1rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@ -50,30 +50,99 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--card: #0a0a0a;
|
||||
--card-foreground: #ededed;
|
||||
--popover: #0a0a0a;
|
||||
--popover-foreground: #ededed;
|
||||
--primary: #ededed;
|
||||
--primary-foreground: #0a0a0a;
|
||||
--secondary: #262626;
|
||||
--secondary-foreground: #ededed;
|
||||
--muted: #262626;
|
||||
--muted-foreground: #a3a3a3;
|
||||
--accent: #262626;
|
||||
--accent-foreground: #ededed;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ededed;
|
||||
--border: #262626;
|
||||
--input: #262626;
|
||||
--ring: #d4d4d8;
|
||||
--background: #0a0a23;
|
||||
--foreground: #ffffff;
|
||||
--card: rgba(255, 255, 255, 0.05);
|
||||
--card-foreground: #ffffff;
|
||||
--popover: rgba(255, 255, 255, 0.05);
|
||||
--popover-foreground: #ffffff;
|
||||
--primary: #ff6b35;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: rgba(255, 255, 255, 0.1);
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: rgba(255, 255, 255, 0.05);
|
||||
--muted-foreground: #cccccc;
|
||||
--accent: #ff6b35;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--input: rgba(255, 255, 255, 0.1);
|
||||
--ring: #ff6b35;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background: linear-gradient(135deg, #0a0a23 0%, #1a1a3a 50%, #2a2a4a 100%);
|
||||
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 = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "分布式流星监测网络 - Distributed Meteor Monitoring Network",
|
||||
description: "连接全球观测者,记录天空中每一道流星的轨迹,为科学研究提供珍贵数据",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -25,7 +25,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
@ -3,18 +3,58 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function Home() {
|
||||
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 (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="min-h-screen flex flex-col relative">
|
||||
<div id="starfield" className="starfield"></div>
|
||||
|
||||
{/* Navigation */}
|
||||
<header className="absolute top-0 right-0 p-6 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<span className="text-sm text-gray-300">
|
||||
Welcome, {user?.email}
|
||||
</span>
|
||||
<Button asChild>
|
||||
@ -34,28 +74,88 @@ export default function Home() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<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">
|
||||
<main className="text-center px-4">
|
||||
<h1 className="text-6xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
分布式流星监测网络
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
|
||||
Distributed Meteor Monitoring Network
|
||||
</p>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button size="lg" asChild>
|
||||
<Link href="/register">Join the Network</Link>
|
||||
{/* Hero Section */}
|
||||
<section className="min-h-screen flex flex-col justify-center items-center text-center px-4 relative">
|
||||
<h1 className="text-6xl font-bold mb-4 hero-title-gradient">
|
||||
分布式流星监测网络
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-8 max-w-2xl">
|
||||
连接全球观测者,记录天空中每一道流星的轨迹,为科学研究提供珍贵数据
|
||||
</p>
|
||||
<p className="text-lg text-gray-400 mb-8">
|
||||
Distributed Meteor Monitoring Network
|
||||
</p>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<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 variant="outline" size="lg" asChild>
|
||||
<Link href="/login">Sign In</Link>
|
||||
<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="/gallery">了解更多</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</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