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
Build and Push Docker Image / build (push) Successful in 2m31s
This commit is contained in:
+6
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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... ---")
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{{template "zitie" .}}
|
||||
{{template "learn_number" .}}
|
||||
{{template "football" .}}
|
||||
{{template "orbit_viewer" .}}
|
||||
|
||||
<script>
|
||||
// 首页 Tile 渲染
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user