feat(orbitviewer): add satellite orbit viewer with 2D track, 10-day pass prediction, and AR sky view
Build and Push Docker Image / build (push) Successful in 2m31s

This commit is contained in:
2026-09-23 00:06:01 -07:00
parent c4caed6569
commit fa3dc66278
17 changed files with 4381 additions and 95 deletions
+6 -1
View File
@@ -15,6 +15,7 @@ import (
"toolbox/pkg/base"
_ "toolbox/pkg/football"
_ "toolbox/pkg/learnnumber"
_ "toolbox/pkg/orbitviewer"
_ "toolbox/pkg/zitie" // 匿名导入以触发 init()
"github.com/gin-gonic/gin"
@@ -104,6 +105,10 @@ func main() {
title = "2026世界杯小组出线模拟器 - 蒙特卡洛预测"
desc = "使用蒙特卡洛算法模拟2026年美加墨世界杯小组赛出线概率,支持自定义比赛概率、小组球队及已有赛果。"
keywords = "2026世界杯, 小组出线概率, 蒙特卡洛模拟, 足球预测"
case "/orbit-viewer":
title = "卫星轨道观测与AR实景寻星 - 实时轨迹与过境预测"
desc = "实时模拟空间站与卫星投影地面轨迹,精准预测未来10天最佳肉眼可见过境窗口,并支持手机AR实景摄像头与陀螺仪定向寻星。"
keywords = "卫星过境预测, 空间站追踪, ISS轨迹, AR寻星, 卫星观测, 轨道投影, 空间望远镜"
}
data := gin.H{
@@ -156,7 +161,7 @@ func main() {
finalPort = "8080"
}
log.Printf("Toolbox server starting on :%s", finalPort)
log.Printf("Toolbox HTTP server starting on :%s -> http://localhost:%s", finalPort, finalPort)
if err := r.Run(":" + finalPort); err != nil {
log.Fatalf("Server failed: %v", err)
}
+63
View File
@@ -4,11 +4,17 @@ import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
"time"
"toolbox/pkg/base"
"github.com/gin-gonic/gin"
)
const (
@@ -16,6 +22,63 @@ const (
baseURL = "http://localhost:" + testPort
)
func TestOrbitViewerTemplatesAndRoutes(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
for id, tool := range base.Registry {
if err := tool.Init(); err != nil {
t.Fatalf("Failed to initialize tool %s: %v", id, err)
}
tool.RegisterRoutes(r.Group("/api/" + id))
}
subFS, err := fs.Sub(webFS, "web")
if err != nil {
t.Fatalf("Failed to fs.Sub: %v", err)
}
tmpl, err := template.ParseFS(subFS, "layout.html", "index.html", "tools/*.html")
if err != nil {
t.Fatalf("Failed to parse templates: %v", err)
}
serveIndex := func(c *gin.Context) {
data := gin.H{
"Title": "测试",
"Description": "描述",
"Keywords": "关键词",
"CanonicalURL": "https://toolbox.pengzhan.dev/orbit-viewer",
"GA_ID": "",
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil {
c.String(http.StatusInternalServerError, "Template error: %v", err)
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
}
r.GET("/orbit-viewer", serveIndex)
// Test GET /orbit-viewer
req, _ := http.NewRequest("GET", "/orbit-viewer", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200 for /orbit-viewer, got %d: %s", w.Code, w.Body.String())
}
bodyStr := w.Body.String()
if !bytes.Contains([]byte(bodyStr), []byte("panel-orbit-viewer")) {
t.Errorf("Expected body to contain panel-orbit-viewer")
}
if !bytes.Contains([]byte(bodyStr), []byte("阶段一:卫星轨道与全球投影轨迹")) {
t.Errorf("Expected body to contain orbit viewer content")
}
}
func TestToolboxE2E(t *testing.T) {
// 1. Build the binary
fmt.Println("--- Step 1: Building binary... ---")
+1
View File
@@ -15,6 +15,7 @@
{{template "zitie" .}}
{{template "learn_number" .}}
{{template "football" .}}
{{template "orbit_viewer" .}}
<script>
// 首页 Tile 渲染
+14 -4
View File
@@ -57,19 +57,21 @@
.nav-item:hover { background: rgba(0,0,0,0.04); color: #000; }
.nav-item.active { background: var(--apple-blue); color: #ffffff; }
.main-container { flex: 1; display: flex; flex-direction: column; overflow-y: auto; width: 100%; }
.main-content { padding: 40px 60px; width: 100%; }
.main-container { flex: 1; display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; width: 100%; max-width: 100vw; }
.main-content { padding: 40px 60px; width: 100%; max-width: 100%; box-sizing: border-box; }
/* Unified Page Header */
.page-header { margin-bottom: 32px; }
.page-header h1 { font-size: 34px; font-weight: 700; margin: 0 0 8px 0; }
.page-header h1 { font-size: 34px; font-weight: 700; margin: 0 0 8px 0; word-break: break-word; }
.page-header p { font-size: 18px; color: #86868b; margin: 0; }
@media (max-width: 768px) {
.sidebar { position: fixed; left: -260px; height: 100%; }
#menu-toggle:checked ~ .sidebar { transform: translateX(260px); }
.menu-btn { display: block; }
.main-content { padding: 80px 20px 20px; }
.main-content { padding: 70px 14px 20px; width: 100vw; max-width: 100vw; box-sizing: border-box; overflow-x: hidden; }
.page-header h1 { font-size: 24px; }
.page-header p { font-size: 14px; }
}
.tool-panel { display: none; }
@@ -164,6 +166,11 @@
title: '2026世界杯小组出线模拟器',
desc: '使用蒙特卡洛算法模拟2026年美加墨世界杯小组赛出线概率,支持自定义比赛概率、小组球队及已有赛果。',
keywords: '2026世界杯, 小组出线概率, 蒙特卡洛模拟, 足球预测'
},
'orbit-viewer': {
title: '卫星轨道观测与AR实景寻星',
desc: '实时模拟空间站与卫星投影地面轨迹,精准预测未来10天最佳肉眼可见过境窗口,并支持手机AR实景摄像头与陀螺仪定向寻星。',
keywords: '卫星过境预测, 空间站追踪, ISS轨迹, AR寻星, 卫星观测, 轨道投影, 空间望远镜'
}
};
@@ -199,6 +206,9 @@
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const activeNav = document.getElementById(`nav-${path}`) || document.getElementById('nav-welcome');
if (activeNav) activeNav.classList.add('active');
// 触发自定义面板切换事件,通知工具初始化或重设尺寸
window.dispatchEvent(new CustomEvent('panel-changed', { detail: { path: path } }));
}
window.onpopstate = renderCurrentPath;
File diff suppressed because it is too large Load Diff