Files
web/server/handlers/homepage.go

341 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "宇恒一号",
NavLinks: []models.NavLink{{Label: "产品简介", URL: "#intro"}, {Label: "产品视频", URL: "#videos"}, {Label: "联系我们", URL: "#contact"}},
Title: "宇恒一号",
Subtitle: "",
Description: "",
DownloadText: "下载",
DownloadURL: "#",
Platforms: []models.PlatformItem{},
Version: "",
LaunchYear: "发布日期:以官网为准",
BadgeText: "完全免费",
DownloadWindowsURL: "/promotion/downloads/yuheng-windows.zip",
DownloadAndroidURL: "/promotion/downloads/yuheng-android.apk",
Features: []models.FeatureItem{
{Title: "星际导航", Desc: "先进的 AI 导航系统,精准定位您的需求,引领探索之旅"},
{Title: "量子同步", Desc: "跨维度数据同步技术,您的数据在多宇宙中保持一致"},
{Title: "星际防护", Desc: "来自未来的安全加密协议,守护您的数字资产安全"},
},
FooterText: "© 2024 宇恒一号 · 成都宇信达智能科技有限公司",
}
}
// 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>
`