宇恒一号官网

This commit is contained in:
whm
2026-03-17 00:59:32 +08:00
commit eb56519df7
105 changed files with 10783 additions and 0 deletions

345
server/handlers/homepage.go Normal file
View File

@@ -0,0 +1,345 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const homepageSlug = "index"
const officialSiteConfigID = "official_site_id"
type officialSiteDoc struct {
ID string `bson:"_id"`
SiteID string `bson:"site_id"`
}
// getOfficialSiteID 从 system_config 读取官网站点 ID未设置则返回第一个站点的 ID
func getOfficialSiteID(ctx context.Context) string {
coll := config.GetDB(config.DBName).Collection("system_config")
var doc officialSiteDoc
err := coll.FindOne(ctx, bson.M{"_id": officialSiteConfigID}).Decode(&doc)
if err == nil && doc.SiteID != "" {
return doc.SiteID
}
sitesColl := config.GetDB(config.DBName).Collection("sites")
opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
var site models.Site
if err := sitesColl.FindOne(ctx, bson.M{}, opts).Decode(&site); err == nil {
return site.ID.Hex()
}
return ""
}
// GetWebHomepage 前台:获取官网站点首页数据(无需鉴权)
func GetWebHomepage(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
siteID := getOfficialSiteID(ctx)
if siteID == "" {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var data models.HomepageData
if page.Content != "" {
if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
} else {
data = defaultHomepageData()
}
c.JSON(http.StatusOK, data)
}
// GetHomepage 获取站点首页数据
func GetHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var data models.HomepageData
if page.Content != "" {
if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
} else {
data = defaultHomepageData()
}
c.JSON(http.StatusOK, data)
}
// UpdateHomepage 更新站点首页数据
func UpdateHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
var data models.HomepageData
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
body, err := json.Marshal(data)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
now := time.Now().Format(time.RFC3339)
filter := bson.M{"site_id": siteID, "slug": homepageSlug}
update := bson.M{
"$set": bson.M{
"site_id": siteID,
"slug": homepageSlug,
"title": data.Title,
"type": "homepage",
"content": string(body),
"updated_at": now,
},
}
opts := options.UpdateOne().SetUpsert(true)
_, err = coll.UpdateOne(ctx, filter, update, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "保存成功"})
}
// DownloadHomepage 下载首页 HTML
func DownloadHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
var data models.HomepageData
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err == nil && page.Content != "" {
_ = json.Unmarshal([]byte(page.Content), &data)
}
if err != nil || page.Content == "" {
data = defaultHomepageData()
}
html := renderHomepageHTML(&data)
c.Header("Content-Disposition", "attachment; filename=index.html")
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
}
func defaultHomepageData() models.HomepageData {
return models.HomepageData{
LogoText: "YUHENG ONE",
NavLinks: []models.NavLink{{Label: "MISSION", URL: "#"}, {Label: "DOWNLOAD", URL: "#"}, {Label: "CONTACT", URL: "#"}},
Title: "宇恒一号",
Subtitle: "INTERSTELLAR EXPLORER EDITION",
Description: "跨越星际的智能伙伴 · 探索无限可能<br>\n 引领您进入前所未有的数字宇宙",
DownloadText: "START EXPLORING",
DownloadURL: "#",
Platforms: []models.PlatformItem{
{Name: "WINDOWS", URL: "#"},
{Name: "MACOS", URL: "#"},
{Name: "LINUX", URL: "#"},
{Name: "IOS", URL: "#"},
{Name: "ANDROID", URL: "#"},
},
Version: "VERSION 3.2.1",
LaunchYear: "LAUNCH: 2024",
BadgeText: "FREE ACCESS",
Features: []models.FeatureItem{
{Title: "星际导航", Desc: "先进的AI导航系统精准定位您的需求引领探索之旅"},
{Title: "量子同步", Desc: "跨维度数据同步技术,您的数据在多宇宙中保持一致"},
{Title: "星际防护", Desc: "来自未来的安全加密协议,守护您的数字资产安全"},
},
FooterText: "© 2024 YUHENG ONE // STELLAR EXPLORATION INITIATIVE",
}
}
// renderHomepageHTML 根据数据生成首页 HTML简化版保留原样式与结构
func renderHomepageHTML(d *models.HomepageData) string {
if d == nil {
d = &models.HomepageData{}
}
titleChars := splitTitle(d.Title)
navHTML := ""
for _, l := range d.NavLinks {
navHTML += `<a href="` + escape(l.URL) + `" style="color: rgba(255,255,255,0.5); text-decoration: none; transition: color 0.3s;">` + escape(l.Label) + `</a>`
}
platformsHTML := ""
for _, p := range d.Platforms {
platformsHTML += `<div class="orbit-platform"><svg viewBox="0 0 24 24"><path d="M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z"/></svg><span>` + escape(p.Name) + `</span></div>`
}
featuresHTML := ""
for i, f := range d.Features {
iconPath := []string{
"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z",
"M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 7.69 9.48 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3s-1.34 3-3 3z",
"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z",
}
if i >= len(iconPath) {
iconPath = append(iconPath, iconPath[0])
}
path := iconPath[i%3]
featuresHTML += `<div class="feature-space"><div class="feature-icon"><svg viewBox="0 0 24 24"><path d="` + path + `"/></svg></div><h3>` + escape(f.Title) + `</h3><p>` + escape(f.Desc) + `</p></div>`
}
sb := &strings.Builder{}
sb.WriteString("<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>")
sb.WriteString(escape(d.Title))
sb.WriteString(" - 星际探索版</title>\n")
sb.WriteString(homepageCSS)
sb.WriteString("</head>\n<body>\n<div class=\"space-bg\"></div>\n<div class=\"stars\" id=\"stars\"></div>\n<div class=\"planet planet-1\"></div>\n<div class=\"planet planet-2\"></div>\n<nav class=\"navbar\"><div class=\"logo-space\">")
sb.WriteString(escape(d.LogoText))
sb.WriteString("</div>\n<div style=\"display: flex; gap: 35px; font-family: 'Exo 2', sans-serif; font-size: 12px; letter-spacing: 2px;\">")
sb.WriteString(navHTML)
sb.WriteString("</div>\n</nav>\n<section class=\"hero\">\n<div class=\"title-container\"><h1 class=\"title-3d\">")
for _, ch := range titleChars {
sb.WriteString("<span>" + escape(ch) + "</span>")
}
sb.WriteString("</h1>\n</div>\n<p class=\"subtitle-space\">")
sb.WriteString(escape(d.Subtitle))
sb.WriteString("</p>\n<p class=\"description-space\">")
sb.WriteString(strings.ReplaceAll(escape(d.Description), "\n", "<br>\n"))
sb.WriteString("</p>\n<div class=\"download-warp\">\n<div class=\"warp-effect\"></div>\n<a href=\"")
sb.WriteString(escape(d.DownloadURL))
sb.WriteString("\" class=\"warp-btn\">\n<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"/></svg>\n")
sb.WriteString(escape(d.DownloadText))
sb.WriteString("\n</a>\n</div>\n<div class=\"orbit-platforms\">")
sb.WriteString(platformsHTML)
sb.WriteString("</div>\n<div class=\"mission-info\">\n<span class=\"mission-badge\">")
sb.WriteString(escape(d.Version))
sb.WriteString("</span>\n<span>🚀 ")
sb.WriteString(escape(d.LaunchYear))
sb.WriteString("</span>\n<span>⚡ ")
sb.WriteString(escape(d.BadgeText))
sb.WriteString("</span>\n</div>\n<div class=\"features-space\">")
sb.WriteString(featuresHTML)
sb.WriteString("</div>\n</section>\n<footer>\n<p>")
sb.WriteString(escape(d.FooterText))
sb.WriteString("</p>\n</footer>\n<script>\nconst starsContainer = document.getElementById('stars');\nfor (let i = 0; i < 200; i++) {\nconst star = document.createElement('div');\nstar.className = 'star';\nstar.style.left = Math.random() * 100 + '%';\nstar.style.top = Math.random() * 100 + '%';\nstar.style.width = (Math.random() * 2 + 1) + 'px';\nstar.style.height = star.style.width;\nstar.style.setProperty('--duration', (Math.random() * 3 + 2) + 's');\nstar.style.setProperty('--min-opacity', Math.random() * 0.3 + 0.1);\nstarsContainer.appendChild(star);\n}\n</script>\n</body>\n</html>")
return sb.String()
}
func splitTitle(s string) []string {
var out []rune
for _, r := range s {
out = append(out, r)
}
if len(out) == 0 {
return []string{"宇", "恒", "一", "号"}
}
result := make([]string, len(out))
for i, r := range out {
result[i] = string(r)
}
return result
}
func escape(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
return s
}
const homepageCSS = `<style>
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;700;900&family=Noto+Sans+SC:wght@300;400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
:root{--space-dark:#0a0a12;--space-blue:#1e3a5f;--nebula-purple:#4a1a6b;--star-white:#fff;--plasma-cyan:#00d4ff;--plasma-pink:#ff2d95;}
body{font-family:'Noto Sans SC',sans-serif;background:var(--space-dark);color:var(--star-white);min-height:100vh;overflow-x:hidden;}
.space-bg{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;background:radial-gradient(ellipse at 20% 80%,rgba(74,26,107,0.3) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(30,58,95,0.3) 0%,transparent 50%);}
.stars{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;pointer-events:none;}
.star{position:absolute;background:#fff;border-radius:50%;animation:twinkle 3s ease-in-out infinite;}
@keyframes twinkle{0%,100%{opacity:0.3;transform:scale(1);}50%{opacity:1;transform:scale(1.2);}}
.planet{position:fixed;border-radius:50%;z-index:2;pointer-events:none;}
.planet-1{width:300px;height:300px;top:10%;right:-100px;background:linear-gradient(135deg,var(--nebula-purple),#1a0a2e);opacity:0.6;}
.planet-2{width:150px;height:150px;bottom:20%;left:-50px;background:linear-gradient(135deg,var(--space-blue),#0a1520);opacity:0.5;}
.navbar{position:fixed;top:0;left:0;right:0;padding:25px 50px;display:flex;justify-content:space-between;align-items:center;z-index:100;background:linear-gradient(180deg,rgba(10,10,18,0.9) 0%,transparent 100%);}
.logo-space{font-family:'Exo 2',sans-serif;font-size:26px;font-weight:900;background:linear-gradient(90deg,var(--plasma-cyan),var(--star-white),var(--plasma-pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:4px;}
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:100px 20px;position:relative;z-index:10;}
.title-container{perspective:1000px;margin-bottom:30px;}
.title-3d{font-family:'Exo 2',sans-serif;font-size:clamp(60px,12vw,150px);font-weight:900;color:var(--star-white);text-shadow:0 0 20px rgba(0,212,255,0.3);}
.subtitle-space{font-family:'Exo 2',sans-serif;font-size:clamp(16px,3vw,24px);letter-spacing:12px;color:var(--plasma-cyan);margin-bottom:25px;}
.description-space{max-width:650px;text-align:center;color:rgba(255,255,255,0.6);line-height:2;font-size:16px;margin-bottom:50px;}
.download-warp{position:relative;display:inline-block;padding:0;background:transparent;border:none;cursor:pointer;}
.download-warp .warp-btn{display:flex;align-items:center;gap:15px;padding:22px 45px;font-size:16px;font-weight:700;color:var(--space-dark);background:linear-gradient(135deg,var(--plasma-cyan),var(--plasma-pink));border:none;font-family:'Exo 2',sans-serif;letter-spacing:2px;text-decoration:none;clip-path:polygon(20px 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%,0 20px);transition:all 0.4s;}
.download-warp .warp-btn:hover{transform:scale(1.05);box-shadow:0 0 30px rgba(0,212,255,0.6);}
.warp-effect{position:absolute;top:50%;left:50%;width:0;height:0;background:radial-gradient(circle,rgba(255,255,255,0.8) 0%,transparent 70%);border-radius:50%;transform:translate(-50%,-50%);animation:warp-drive 1.5s ease-out infinite;}
@keyframes warp-drive{0%{width:0;height:0;opacity:1;}100%{width:300px;height:300px;opacity:0;}}
.orbit-platforms{display:flex;gap:25px;margin-top:70px;flex-wrap:wrap;justify-content:center;}
.orbit-platform{width:90px;height:90px;background:rgba(30,58,95,0.3);border:1px solid rgba(0,212,255,0.3);border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:all 0.4s;}
.orbit-platform:hover{background:rgba(0,212,255,0.2);border-color:var(--plasma-cyan);transform:translateY(-10px);}
.orbit-platform svg{width:28px;height:28px;fill:var(--star-white);}
.orbit-platform span{font-size:10px;color:rgba(255,255,255,0.6);}
.mission-info{margin-top:60px;display:flex;gap:40px;flex-wrap:wrap;justify-content:center;font-size:13px;color:rgba(255,255,255,0.4);}
.mission-info .mission-badge{padding:4px 12px;background:rgba(0,212,255,0.1);border:1px solid rgba(0,212,255,0.3);border-radius:20px;color:var(--plasma-cyan);font-size:11px;}
.features-space{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:30px;max-width:1100px;margin:80px auto 0;padding:0 20px;}
.feature-space{background:linear-gradient(135deg,rgba(30,58,95,0.2),rgba(10,10,18,0.8));border:1px solid rgba(0,212,255,0.1);border-radius:20px;padding:35px;transition:all 0.5s;}
.feature-space:hover{transform:translateY(-15px);border-color:rgba(0,212,255,0.4);}
.feature-icon{width:55px;height:55px;background:linear-gradient(135deg,var(--plasma-cyan),var(--plasma-pink));border-radius:15px;display:flex;align-items:center;justify-content:center;margin-bottom:20px;}
.feature-icon svg{width:28px;height:28px;fill:var(--space-dark);}
.feature-space h3{font-family:'Exo 2',sans-serif;font-size:18px;color:var(--star-white);margin-bottom:12px;}
.feature-space p{color:rgba(255,255,255,0.5);font-size:14px;line-height:1.7;}
footer{padding:40px;text-align:center;border-top:1px solid rgba(255,255,255,0.05);margin-top:80px;}
footer p{color:rgba(255,255,255,0.3);font-size:12px;}
@media(max-width:768px){.planet{display:none;}}
</style>
`