Merge pull request #341 from qinfendebingshuo/copilot/deploy-cn-domain-to-guangzhou-server

feat: 国内域名ICP备案落地页部署 (广州 ZY-SVR-004)
This commit is contained in:
冰朔 2026-04-11 21:05:03 +08:00 committed by GitHub
commit b85aecdbc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 731 additions and 0 deletions

313
.github/workflows/deploy-cn-landing.yml vendored Normal file
View File

@ -0,0 +1,313 @@
name: 🇨🇳 国内域名落地页 · 部署
# ═══════════════════════════════════════════════════════════
# 国内域名ICP备案落地页部署工作流
# 编号: ZY-WF-CN-LANDING
# 服务器: ZY-SVR-004 · 43.139.217.141 · 广州七区
# 守护: 铸渊 · ICE-GL-ZY001
# 版权: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════════════════
#
# 部署内容:
# 1. 国内落地页 (server/sites/cn-landing/) → /opt/zhuyuan-cn/sites/cn-landing/
# 2. Nginx 配置 (server/nginx/cn-domain.conf) → Nginx sites-enabled
# 3. LLM API 密钥注入 (四大国内模型)
#
# 架构: GitHub Actions(美国) → SG(新加坡·跳板) → CN(广州·目标)
on:
push:
branches: [main]
paths:
- 'server/sites/cn-landing/**'
- 'server/nginx/cn-domain.conf'
workflow_dispatch:
inputs:
action:
description: '部署动作'
required: true
default: 'deploy'
type: choice
options:
- deploy
- health-check
concurrency:
group: cn-landing-deploy
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
name: 🚀 部署国内落地页
if: github.event.inputs.action == 'deploy' || github.event.inputs.action == '' || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 🔐 配置SSH跳板 (SG→CN)
env:
SG_SSH_KEY: ${{ secrets.ZY_SERVER_KEY }}
CN_SSH_KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
CN_SSH_PORT: ${{ secrets.ZY_CN_SSH_PORT }}
run: |
CN_PORT="${CN_SSH_PORT:-22}"
echo "🔧 SSH跳板: GitHub Actions → SG → CN (端口 ${CN_PORT})"
mkdir -p ~/.ssh
echo "$SG_SSH_KEY" > ~/.ssh/id_sg
echo "$CN_SSH_KEY" > ~/.ssh/id_cn
chmod 600 ~/.ssh/id_sg ~/.ssh/id_cn
for key_file in id_sg id_cn; do
if ! head -1 ~/.ssh/${key_file} | grep -q "BEGIN"; then
echo "❌ SSH密钥格式异常: ${key_file}"
exit 1
fi
done
echo "✅ SG + CN 密钥格式正确"
ssh-keyscan -H ${{ secrets.ZY_SERVER_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
cat > ~/.ssh/config << EOF
Host sg-jump
HostName ${{ secrets.ZY_SERVER_HOST }}
User ${{ secrets.ZY_SERVER_USER }}
IdentityFile ~/.ssh/id_sg
StrictHostKeyChecking accept-new
BatchMode yes
Host cn-target
HostName ${{ secrets.ZY_CN_SERVER_HOST }}
User ${{ secrets.ZY_CN_SERVER_USER }}
Port ${CN_PORT}
IdentityFile ~/.ssh/id_cn
ProxyJump sg-jump
StrictHostKeyChecking accept-new
BatchMode yes
ConnectTimeout 30
EOF
chmod 600 ~/.ssh/config
- name: 🔍 SSH连接测试
run: |
echo "🔍 测试SSH跳板连接到广州服务器..."
ssh cn-target 'echo "✅ SSH连接成功 · $(hostname) · $(date)"' || {
echo "❌ SSH跳板连接失败"
exit 1
}
- name: 📂 准备目标目录
run: |
ssh cn-target << 'PREP_CMD'
set -e
mkdir -p /opt/zhuyuan-cn/sites/cn-landing
mkdir -p /opt/zhuyuan-cn/data/logs
mkdir -p /opt/zhuyuan-cn/config/nginx
echo "✅ 目录结构就绪"
PREP_CMD
- name: 📦 同步落地页文件
run: |
rsync -avz --delete \
-e "ssh -F ~/.ssh/config" \
server/sites/cn-landing/ \
cn-target:/opt/zhuyuan-cn/sites/cn-landing/
echo "✅ 落地页已同步到 /opt/zhuyuan-cn/sites/cn-landing/"
- name: 📦 同步Nginx配置
env:
ZY_CN_DOMAIN: ${{ secrets.ZY_CN_DOMAIN }}
run: |
cp server/nginx/cn-domain.conf /tmp/cn-domain.conf
if [ -n "$ZY_CN_DOMAIN" ]; then
sed -i "s/ZY_CN_DOMAIN_PLACEHOLDER/${ZY_CN_DOMAIN}/g" /tmp/cn-domain.conf
echo "✅ 国内域名已注入: ${ZY_CN_DOMAIN}"
else
echo "⚠️ ZY_CN_DOMAIN 未配置使用IP直接访问"
sed -i "s/ZY_CN_DOMAIN_PLACEHOLDER/_/g" /tmp/cn-domain.conf
fi
scp -F ~/.ssh/config \
/tmp/cn-domain.conf \
cn-target:/opt/zhuyuan-cn/config/nginx/cn-domain.conf
echo "✅ Nginx配置已上传"
- name: 🔑 注入LLM API密钥
env:
ZY_DEEPSEEK_API_KEY: ${{ secrets.ZY_DEEPSEEK_API_KEY }}
ZY_QIANWEN_API_KEY: ${{ secrets.ZY_QIANWEN_API_KEY }}
ZY_KIMI_API_KEY: ${{ secrets.ZY_KIMI_API_KEY }}
ZY_QINGYAN_API_KEY: ${{ secrets.ZY_QINGYAN_API_KEY }}
run: |
# 在本地生成密钥文件(变量在此展开)
cat > /tmp/.env.llm << ENVEOF
ZY_DEEPSEEK_API_KEY=${ZY_DEEPSEEK_API_KEY}
ZY_QIANWEN_API_KEY=${ZY_QIANWEN_API_KEY}
ZY_KIMI_API_KEY=${ZY_KIMI_API_KEY}
ZY_QINGYAN_API_KEY=${ZY_QINGYAN_API_KEY}
ENVEOF
# 上传到目标服务器
scp -F ~/.ssh/config /tmp/.env.llm cn-target:/opt/zhuyuan-cn/config/.env.llm
ssh cn-target 'chmod 600 /opt/zhuyuan-cn/config/.env.llm'
rm -f /tmp/.env.llm
echo "✅ 四大国内模型API密钥已注入"
- name: 🔧 启用Nginx配置 & 重载
run: |
ssh cn-target << 'NGINX_CMD'
set -e
# 复制配置到Nginx目录
sudo cp /opt/zhuyuan-cn/config/nginx/cn-domain.conf /etc/nginx/sites-available/cn-domain.conf
sudo ln -sf /etc/nginx/sites-available/cn-domain.conf /etc/nginx/sites-enabled/cn-domain.conf
# 移除默认配置,避免 default_server 冲突
sudo rm -f /etc/nginx/sites-enabled/default
# 测试并重载Nginx
sudo nginx -t && sudo systemctl reload nginx
echo "✅ Nginx配置已生效"
NGINX_CMD
- name: 💚 健康检查
run: |
ssh cn-target << 'HEALTH_CMD'
set -e
echo "═══ 国内落地页部署验证 ═══"
# 检查Nginx
if systemctl is-active --quiet nginx; then
echo "✅ Nginx: 运行中"
else
echo "❌ Nginx: 已停止"
exit 1
fi
# 检查页面可访问
HTTP_CODE=$(curl -sf -o /dev/null -w '%{http_code}' http://localhost/ 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ 落地页: HTTP 200 OK"
else
echo "❌ 落地页: HTTP ${HTTP_CODE}"
exit 1
fi
# 检查ICP备案号
if curl -sf http://localhost/ | grep -q "陕ICP备2025071211号"; then
echo "✅ ICP备案号: 已显示"
else
echo "⚠️ ICP备案号: 未找到(请检查页面内容)"
fi
# 检查备案链接
if curl -sf http://localhost/ | grep -q "beian.miit.gov.cn"; then
echo "✅ 工信部链接: 已配置"
else
echo "⚠️ 工信部链接: 未找到"
fi
# 健康端点
HEALTH=$(curl -sf http://localhost/health 2>/dev/null || echo '{}')
echo "✅ 健康探针: ${HEALTH}"
echo ""
echo "═══ 部署完成 ═══"
HEALTH_CMD
- name: 🧹 清理SSH密钥
if: always()
run: rm -f ~/.ssh/id_sg ~/.ssh/id_cn ~/.ssh/config
health-check:
name: 💚 国内落地页健康检查
if: github.event.inputs.action == 'health-check'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 🔐 配置SSH跳板 (SG→CN)
env:
SG_SSH_KEY: ${{ secrets.ZY_SERVER_KEY }}
CN_SSH_KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
CN_SSH_PORT: ${{ secrets.ZY_CN_SSH_PORT }}
run: |
CN_PORT="${CN_SSH_PORT:-22}"
mkdir -p ~/.ssh
echo "$SG_SSH_KEY" > ~/.ssh/id_sg
echo "$CN_SSH_KEY" > ~/.ssh/id_cn
chmod 600 ~/.ssh/id_sg ~/.ssh/id_cn
ssh-keyscan -H ${{ secrets.ZY_SERVER_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
cat > ~/.ssh/config << EOF
Host sg-jump
HostName ${{ secrets.ZY_SERVER_HOST }}
User ${{ secrets.ZY_SERVER_USER }}
IdentityFile ~/.ssh/id_sg
StrictHostKeyChecking accept-new
BatchMode yes
Host cn-target
HostName ${{ secrets.ZY_CN_SERVER_HOST }}
User ${{ secrets.ZY_CN_SERVER_USER }}
Port ${CN_PORT}
IdentityFile ~/.ssh/id_cn
ProxyJump sg-jump
StrictHostKeyChecking accept-new
BatchMode yes
ConnectTimeout 30
EOF
chmod 600 ~/.ssh/config
- name: 💚 执行健康检查
run: |
ssh cn-target << 'HEALTH_CMD'
echo "═══ 国内落地页健康报告 ═══"
echo ""
echo "§1 系统状态:"
uptime
echo ""
echo "§2 Nginx 状态:"
systemctl is-active nginx && echo "Nginx: 运行中" || echo "Nginx: 已停止"
echo ""
echo "§3 落地页文件:"
ls -la /opt/zhuyuan-cn/sites/cn-landing/ 2>/dev/null || echo "目录不存在"
echo ""
echo "§4 页面访问测试:"
HTTP_CODE=$(curl -sf -o /dev/null -w '%{http_code}' http://localhost/ 2>/dev/null || echo "000")
echo "HTTP状态码: ${HTTP_CODE}"
echo ""
echo "§5 ICP备案号检查:"
if curl -sf http://localhost/ | grep -q "陕ICP备2025071211号"; then
echo "✅ ICP备案号已显示"
else
echo "❌ ICP备案号未找到"
fi
echo ""
echo "§6 LLM API密钥状态:"
if [ -f /opt/zhuyuan-cn/config/.env.llm ]; then
KEY_COUNT=$(grep -c "API_KEY" /opt/zhuyuan-cn/config/.env.llm 2>/dev/null || echo "0")
echo "已配置密钥数: ${KEY_COUNT}"
else
echo "⚠️ LLM密钥文件不存在"
fi
HEALTH_CMD
- name: 🧹 清理SSH密钥
if: always()
run: rm -f ~/.ssh/id_sg ~/.ssh/id_cn ~/.ssh/config

View File

@ -0,0 +1,59 @@
# ═══════════════════════════════════════════════════════════
# 国内域名 · ICP备案落地页 Nginx 配置
# ═══════════════════════════════════════════════════════════
#
# 编号: ZY-SVR-NGX-CN-LANDING
# 服务器: ZY-SVR-004 · 43.139.217.141 · 广州七区
# 守护: 铸渊 · ICE-GL-ZY001
# 版权: 国作登字-2026-A-00037559
#
# 架构:
# ZY_CN_DOMAIN (www.guanghulab.com) → 国内用户首页
# 前端: /opt/zhuyuan-cn/sites/cn-landing/ (静态HTML)
# 用途: ICP备案合规 + 未来国内语言操作系统入口
#
# 域名变量由 GitHub Secrets 注入:
# ZY_CN_DOMAIN — 国内备案域名
# ═══════════════════════════════════════════════════════════
server {
listen 80 default_server;
server_name ZY_CN_DOMAIN_PLACEHOLDER;
# ─── 安全头 ───
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Server-Identity "ZY-SVR-004" always;
add_header X-Site-Mode "cn-landing" always;
# ─── 静态文件根目录 · 国内落地页 ───
root /opt/zhuyuan-cn/sites/cn-landing;
index index.html;
# ─── 前端静态文件 ───
location / {
try_files $uri $uri/ /index.html;
}
# ─── 健康探针 ───
location = /health {
access_log off;
return 200 '{"status":"ok","server":"ZY-SVR-004","role":"cn-landing"}';
add_header Content-Type application/json;
}
# ─── 静态资源缓存 ───
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# ─── 错误页面 ───
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
# ─── 访问日志 ───
access_log /opt/zhuyuan-cn/data/logs/nginx-cn-landing.log;
error_log /opt/zhuyuan-cn/data/logs/nginx-cn-landing-error.log;
}

View File

@ -89,6 +89,18 @@
"notes": "Awen下载完整仓库后部署·铸渊主控自动唤醒"
}
],
"cn_domain": {
"description": "国内ICP备案域名 · 广州落地页",
"domain_secret": "ZY_CN_DOMAIN",
"server": "ZY-SVR-004",
"icp_number": "陕ICP备2025071211号",
"nginx_config": "server/nginx/cn-domain.conf",
"landing_page": "server/sites/cn-landing/index.html",
"workflow": ".github/workflows/deploy-cn-landing.yml",
"deploy_path": "/opt/zhuyuan-cn/sites/cn-landing/",
"status": "deploying",
"notes": "未来将替代guanghulab.online成为国内用户首页入口"
},
"domain_proxy": {
"description": "域名未备案反向代理 · 新加坡中转架构",
"architecture": "用户 → 域名(DNS→SG) → ZY-SVR-002 Nginx → 广州后端",

View File

@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>光湖实验室 · GuanghuLab</title>
<meta name="description" content="光湖实验室 — 语言驱动的智能创作平台">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌊</text></svg>">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg-deep:#070b1a;
--glow-cyan:#22d3ee;
--glow-purple:#a78bfa;
--glow-blue:#3b82f6;
--glow-amber:#fbbf24;
--text-primary:#e8edf5;
--text-secondary:#8b95a8;
--text-dim:#4a5568;
--glass:rgba(12,20,40,0.55);
--glass-border:rgba(120,140,180,0.12);
--font-body:'Segoe UI','PingFang SC','Microsoft YaHei','Helvetica Neue',sans-serif;
--font-mono:'JetBrains Mono','Fira Code',monospace;
}
html{font-size:16px}
body{
font-family:var(--font-body);background:var(--bg-deep);color:var(--text-primary);
min-height:100vh;overflow-x:hidden;display:flex;flex-direction:column;
align-items:center;justify-content:center;position:relative;
}
/* ═══ 星空画布 ═══ */
#starfield{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none}
/* ═══ 星云 ═══ */
.nebula{position:fixed;border-radius:50%;filter:blur(100px);pointer-events:none;z-index:0;animation:nebulaDrift 20s ease-in-out infinite alternate}
.neb-1{width:600px;height:600px;background:radial-gradient(circle,rgba(34,211,238,0.08),transparent);top:-120px;right:-100px}
.neb-2{width:500px;height:500px;background:radial-gradient(circle,rgba(167,139,250,0.07),transparent);bottom:-80px;left:-100px}
.neb-3{width:350px;height:350px;background:radial-gradient(circle,rgba(59,130,246,0.06),transparent);top:35%;left:15%;animation-delay:-8s}
@keyframes nebulaDrift{0%{transform:translate(0,0) scale(1)}50%{transform:translate(30px,-20px) scale(1.05)}100%{transform:translate(-20px,15px) scale(0.95)}}
/* ═══ 光晕粒子 ═══ */
.particle{position:fixed;width:3px;height:3px;border-radius:50%;pointer-events:none;z-index:0}
.particle::after{content:'';position:absolute;width:100%;height:100%;border-radius:50%;background:inherit;filter:blur(4px);opacity:.6}
/* ═══ 主容器 ═══ */
.main-wrap{
position:relative;z-index:1;text-align:center;padding:40px 24px;
max-width:680px;width:100%;
}
/* ═══ Logo 图标 ═══ */
.logo-icon{
font-size:72px;margin-bottom:24px;display:inline-block;
animation:logoFloat 6s ease-in-out infinite;
filter:drop-shadow(0 0 30px rgba(34,211,238,0.3));
}
@keyframes logoFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-12px)}}
/* ═══ 标题 ═══ */
.title{
font-size:clamp(2rem,5vw,3.2rem);font-weight:800;letter-spacing:0.02em;
margin-bottom:8px;line-height:1.3;
background:linear-gradient(135deg,var(--glow-cyan) 0%,var(--glow-blue) 40%,var(--glow-purple) 100%);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
background-size:200% 200%;animation:gradientShift 8s ease-in-out infinite;
}
@keyframes gradientShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.title-en{
font-size:clamp(0.85rem,2vw,1.1rem);font-weight:400;
color:var(--text-secondary);letter-spacing:3px;text-transform:uppercase;
margin-bottom:40px;opacity:.7;
}
/* ═══ 分割线 ═══ */
.divider{
width:120px;height:2px;margin:0 auto 36px;border-radius:2px;
background:linear-gradient(90deg,transparent,var(--glow-cyan),var(--glow-purple),transparent);
animation:dividerPulse 4s ease-in-out infinite;
}
@keyframes dividerPulse{0%,100%{opacity:.5;width:120px}50%{opacity:1;width:180px}}
/* ═══ 简介 ═══ */
.tagline{
font-size:clamp(1rem,2.5vw,1.25rem);color:var(--text-secondary);
line-height:1.8;margin-bottom:48px;font-weight:300;letter-spacing:0.05em;
}
.tagline em{
font-style:normal;color:var(--glow-cyan);font-weight:500;
}
/* ═══ 状态指示 ═══ */
.status-badge{
display:inline-flex;align-items:center;gap:8px;
padding:10px 24px;border-radius:40px;
background:var(--glass);border:1px solid var(--glass-border);
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
font-size:0.85rem;color:var(--text-secondary);
margin-bottom:60px;
animation:badgeFadeIn 1.5s ease-out 0.5s both;
}
.status-dot{
width:8px;height:8px;border-radius:50%;background:#34d399;
box-shadow:0 0 8px rgba(52,211,153,0.5);
animation:dotPulse 2s ease-in-out infinite;
}
@keyframes dotPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(0.8)}}
@keyframes badgeFadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
/* ═══ 装饰环 ═══ */
.orbit-ring{
position:fixed;border-radius:50%;border:1px solid rgba(34,211,238,0.04);
pointer-events:none;z-index:0;
}
.orbit-1{width:500px;height:500px;top:50%;left:50%;transform:translate(-50%,-50%);animation:orbitSpin 60s linear infinite}
.orbit-2{width:700px;height:700px;top:50%;left:50%;transform:translate(-50%,-50%);animation:orbitSpin 90s linear infinite reverse;border-color:rgba(167,139,250,0.03)}
.orbit-3{width:900px;height:900px;top:50%;left:50%;transform:translate(-50%,-50%);animation:orbitSpin 120s linear infinite;border-color:rgba(59,130,246,0.025)}
@keyframes orbitSpin{from{transform:translate(-50%,-50%) rotate(0deg)}to{transform:translate(-50%,-50%) rotate(360deg)}}
/* 轨道上的光点 */
.orbit-dot{
position:absolute;width:4px;height:4px;border-radius:50%;
background:var(--glow-cyan);box-shadow:0 0 10px rgba(34,211,238,0.4);
}
.orbit-1 .orbit-dot{top:0;left:50%;transform:translate(-50%,-50%)}
.orbit-2 .orbit-dot{bottom:0;right:30%;transform:translate(50%,50%)}
/* ═══ 底部备案 ═══ */
.footer{
position:fixed;bottom:0;left:0;width:100%;z-index:10;
padding:20px;text-align:center;
background:linear-gradient(to top,rgba(7,11,26,0.95),transparent);
}
.icp-link{
color:var(--text-dim);font-size:13px;text-decoration:none;
transition:color .3s;display:inline-flex;align-items:center;gap:6px;
}
.icp-link:hover{color:var(--text-secondary)}
.icp-link .icp-icon{font-size:11px;opacity:.6}
/* ═══ 入场动画 ═══ */
.fade-up{animation:fadeUp 1s ease-out both}
.fade-up-d1{animation-delay:0.1s}
.fade-up-d2{animation-delay:0.3s}
.fade-up-d3{animation-delay:0.5s}
.fade-up-d4{animation-delay:0.7s}
.fade-up-d5{animation-delay:0.9s}
@keyframes fadeUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
/* ═══ 响应式 ═══ */
@media(max-width:480px){
.logo-icon{font-size:56px}
.status-badge{padding:8px 18px;font-size:0.8rem}
.orbit-ring{display:none}
}
</style>
</head>
<body>
<!-- 星空画布 -->
<canvas id="starfield"></canvas>
<!-- 星云 -->
<div class="nebula neb-1"></div>
<div class="nebula neb-2"></div>
<div class="nebula neb-3"></div>
<!-- 装饰轨道环 -->
<div class="orbit-ring orbit-1"><div class="orbit-dot"></div></div>
<div class="orbit-ring orbit-2"><div class="orbit-dot"></div></div>
<div class="orbit-ring orbit-3"></div>
<!-- 主内容 -->
<div class="main-wrap">
<div class="logo-icon fade-up fade-up-d1">🌊</div>
<h1 class="title fade-up fade-up-d2">光湖实验室</h1>
<p class="title-en fade-up fade-up-d2">GuanghuLab</p>
<div class="divider fade-up fade-up-d3"></div>
<p class="tagline fade-up fade-up-d4">
<em>语言驱动</em>的智能创作平台<br>
探索人类与AI共生的数字世界
</p>
<div class="status-badge fade-up fade-up-d5">
<span class="status-dot"></span>
系统运行中 · Language-Driven OS
</div>
</div>
<!-- 底部ICP备案 -->
<footer class="footer">
<a class="icp-link" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
<span class="icp-icon">🛡️</span>
陕ICP备2025071211号
</a>
</footer>
<script>
// ═══ 星空粒子系统 ═══
(function(){
const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d');
let stars = [];
let shootingStars = [];
let W, H;
function resize(){
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// 生成静态星星
function createStars(count){
stars = [];
for(let i = 0; i < count; i++){
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.5 + 0.3,
alpha: Math.random() * 0.8 + 0.2,
twinkleSpeed: Math.random() * 0.02 + 0.005,
twinklePhase: Math.random() * Math.PI * 2
});
}
}
createStars(200);
// 流星
function spawnShootingStar(){
if(shootingStars.length > 2) return;
shootingStars.push({
x: Math.random() * W * 0.8,
y: Math.random() * H * 0.3,
len: Math.random() * 80 + 40,
speed: Math.random() * 4 + 3,
angle: Math.PI / 4 + (Math.random() - 0.5) * 0.3,
alpha: 1,
decay: Math.random() * 0.015 + 0.008
});
}
setInterval(spawnShootingStar, 4000);
// 浮动光粒
const floatingParticles = [];
for(let i = 0; i < 15; i++){
floatingParticles.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
r: Math.random() * 2 + 1,
hue: Math.random() > 0.5 ? 187 : 260, // cyan or purple
alpha: Math.random() * 0.4 + 0.1
});
}
let time = 0;
function animate(){
ctx.clearRect(0, 0, W, H);
time += 0.016;
// 绘制星星
for(const s of stars){
const twinkle = Math.sin(time * s.twinkleSpeed * 60 + s.twinklePhase) * 0.3 + 0.7;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(220,230,255,${s.alpha * twinkle})`;
ctx.fill();
}
// 绘制流星
for(let i = shootingStars.length - 1; i >= 0; i--){
const ss = shootingStars[i];
const endX = ss.x - Math.cos(ss.angle) * ss.len;
const endY = ss.y - Math.sin(ss.angle) * ss.len;
const grad = ctx.createLinearGradient(ss.x, ss.y, endX, endY);
grad.addColorStop(0, `rgba(220,240,255,${ss.alpha})`);
grad.addColorStop(1, `rgba(220,240,255,0)`);
ctx.beginPath();
ctx.moveTo(ss.x, ss.y);
ctx.lineTo(endX, endY);
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5;
ctx.stroke();
// 头部光点
ctx.beginPath();
ctx.arc(ss.x, ss.y, 2, 0, Math.PI * 2);
ctx.fillStyle = `rgba(200,230,255,${ss.alpha})`;
ctx.fill();
ss.x += Math.cos(ss.angle) * ss.speed;
ss.y += Math.sin(ss.angle) * ss.speed;
ss.alpha -= ss.decay;
if(ss.alpha <= 0 || ss.x > W || ss.y > H){
shootingStars.splice(i, 1);
}
}
// 绘制浮动光粒
for(const p of floatingParticles){
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
const pulse = Math.sin(time * 2 + p.x * 0.01) * 0.2 + 0.8;
ctx.fillStyle = `hsla(${p.hue},70%,65%,${p.alpha * pulse})`;
ctx.fill();
// 光晕
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue},70%,65%,${p.alpha * pulse * 0.15})`;
ctx.fill();
p.x += p.vx;
p.y += p.vy;
if(p.x < 0 || p.x > W) p.vx *= -1;
if(p.y < 0 || p.y > H) p.vy *= -1;
}
requestAnimationFrame(animate);
}
animate();
// 窗口大小变化时重新生成星星
window.addEventListener('resize', () => createStars(200));
})();
// ═══ 鼠标跟随光晕 ═══
(function(){
const glow = document.createElement('div');
glow.style.cssText = 'position:fixed;width:300px;height:300px;border-radius:50%;pointer-events:none;z-index:0;background:radial-gradient(circle,rgba(34,211,238,0.06),transparent 70%);transition:transform .15s ease-out;will-change:transform;';
document.body.appendChild(glow);
let mx = 0, my = 0;
document.addEventListener('mousemove', e => {
mx = e.clientX; my = e.clientY;
glow.style.transform = `translate(${mx - 150}px, ${my - 150}px)`;
});
})();
</script>
</body>
</html>