add: frontend

This commit is contained in:
Nite07 2024-11-29 03:53:36 +08:00
parent 156b8fbb65
commit 47f6ba2104
8 changed files with 412 additions and 6 deletions

View File

@ -1,11 +1,17 @@
package server package server
import ( import (
"path/filepath"
"pcgamedb/crawler"
"pcgamedb/db"
"pcgamedb/log"
"pcgamedb/server/handler" "pcgamedb/server/handler"
"pcgamedb/server/middleware" "pcgamedb/server/middleware"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"pcgamedb/docs" "pcgamedb/docs"
@ -18,7 +24,65 @@ func initRoute(app *gin.Engine) {
AllowAllOrigins: true, AllowAllOrigins: true,
})) }))
GameInfoGroup := app.Group("/game") initFrontend(app)
initApi(app)
}
func initFrontend(app *gin.Engine) {
app.Static("/static", "server/static")
layoutFiles, err := filepath.Glob("server/templates/layouts/*.tmpl")
if err != nil {
log.Logger.Fatal("Error loading layout templates", zap.Error(err))
return
}
rootFiles, err := filepath.Glob("server/templates/*.tmpl")
if err != nil {
log.Logger.Fatal("Error loading root templates", zap.Error(err))
return
}
app.LoadHTMLFiles(append(layoutFiles, rootFiles...)...)
app.GET("/", func(ctx *gin.Context) {
infos, err := crawler.GetSteam250MonthTop50()
if err != nil {
ctx.HTML(500, "500.tmpl", err)
return
}
ctx.HTML(200, "index.tmpl", gin.H{
"MonthTop": infos,
})
})
app.GET("/game/:id", func(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := primitive.ObjectIDFromHex(idStr)
if err != nil {
ctx.HTML(400, "400.tmpl", nil)
return
}
info, err := db.GetGameInfoByID(id)
if err != nil {
ctx.HTML(500, "500.tmpl", err)
return
}
games, err := db.GetGameItemsByIDs(info.GameIDs)
if err != nil {
ctx.HTML(500, "500.tmpl", err)
return
}
info.Games = games
//TODO: fix this
ctx.HTML(200, "game.tmpl", info)
})
}
func initApi(app *gin.Engine) {
apiGroup := app.Group("/api")
GameInfoGroup := apiGroup.Group("/game")
GameItemGroup := GameInfoGroup.Group("/raw") GameItemGroup := GameInfoGroup.Group("/raw")
GameItemGroup.GET("/unorganized", handler.GetUnorganizedGameItemsHandler) GameItemGroup.GET("/unorganized", handler.GetUnorganizedGameItemsHandler)
@ -33,11 +97,11 @@ func initRoute(app *gin.Engine) {
GameInfoGroup.GET("/id/:id", handler.GetGameInfoByIDHandler) GameInfoGroup.GET("/id/:id", handler.GetGameInfoByIDHandler)
GameInfoGroup.DELETE("/id/:id", middleware.Auth(), handler.DeleteGameInfoHandler) GameInfoGroup.DELETE("/id/:id", middleware.Auth(), handler.DeleteGameInfoHandler)
app.GET("/popular/:type", handler.GetPopularGameInfosHandler) apiGroup.GET("/popular/:type", handler.GetPopularGameInfosHandler)
app.GET("/healthcheck", handler.HealthCheckHandler) apiGroup.GET("/healthcheck", handler.HealthCheckHandler)
app.GET("/author", handler.GetAllAuthorsHandler) apiGroup.GET("/author", handler.GetAllAuthorsHandler)
app.POST("/clean", middleware.Auth(), handler.CleanGameHandler) apiGroup.POST("/clean", middleware.Auth(), handler.CleanGameHandler)
docs.SwaggerInfo.BasePath = "/api" docs.SwaggerInfo.BasePath = "/api"
app.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) apiGroup.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
} }

View File

@ -0,0 +1,2 @@
{{template "base" .}} {{define "title"}}Page Not Found{{end}} {{define
"styles"}} {{end}} {{define "content"}} Page Not Found {{end}}

View File

@ -0,0 +1,2 @@
{{template "base" .}} {{define "title"}}Server Error{{end}} {{define "styles"}}
{{end}} {{define "content"}} {{ . }} {{end}}

189
server/templates/game.html Normal file
View File

@ -0,0 +1,189 @@
{{template "base" .}} {{define "title"}}{{.Name}} - 游戏详情{{end}} {{define
"styles"}}
<style>
.game-cover {
max-height: 400px;
object-fit: cover;
}
.screenshot-gallery {
height: 600px;
}
.swiper-slide img {
width: 100%;
height: 100%;
object-fit: contain;
}
.info-label {
font-weight: bold;
color: #666;
}
.download-card {
transition: transform 0.2s;
}
.download-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.tag {
display: inline-block;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
background-color: #f8f9fa;
border-radius: 0.25rem;
font-size: 0.875rem;
}
</style>
{{end}} {{define "content"}}
<!-- Game Details -->
<div class="container py-4">
<!-- Basic Info -->
<div class="row mb-4">
<div class="col-md-4">
{{if .Cover}}
<img
src="{{.Cover}}"
class="img-fluid rounded game-cover"
alt="{{.Name}}"
/>
{{else}}
<div
class="game-cover bg-secondary d-flex align-items-center justify-content-center rounded"
>
<span class="text-white">暂无封面</span>
</div>
{{end}}
</div>
<div class="col-md-8">
<h1 class="mb-3">{{.Name}}</h1>
{{if .Aliases}}
<div>
<span class="info-label">别名:</span>
{{range .Aliases}}
<span class="tag">{{.}}</span>
{{end}}
</div>
{{end}}
<div>
<span class="info-label">开发商:</span>
{{range .Developers}}
<span class="tag">{{.}}</span>
{{end}}
</div>
<div>
<span class="info-label">发行商:</span>
{{range .Publishers}}
<span class="tag">{{.}}</span>
{{end}}
</div>
{{if .Languages}}
<div>
<span class="info-label">支持语言:</span>
{{range .Languages}}
<span class="tag">{{.}}</span>
{{end}}
</div>
{{end}} {{if .Description}}
<div>
<p>{{.Description}}</p>
</div>
{{end}} {{if .SteamID}}
<div>
<a
href="https://store.steampowered.com/app/{{.SteamID}}"
target="_blank"
class="btn btn-primary"
>
在 Steam 上查看
</a>
</div>
{{end}}
</div>
</div>
<!-- Screenshots -->
{{if .Screenshots}}
<div class="mb-4">
<h3 class="mb-3">游戏截图</h3>
<div class="swiper screenshot-gallery">
<div class="swiper-wrapper">
{{range .Screenshots}}
<div class="swiper-slide">
<img src="{{.}}" alt="游戏截图" />
</div>
{{end}}
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</div>
{{end}}
<!-- Download Links -->
{{if .Games}}
<div class="mb-4">
<h3 class="mb-3">下载链接</h3>
<div class="row g-4">
{{range .Games}}
<div class="col-md-6">
<div class="card download-card">
<div class="card-body">
<h5 class="card-title">{{.Name}}</h5>
{{if .Size}}
<p class="card-text">
<small class="text-muted">文件大小:{{.Size}}</small>
</p>
{{end}} {{if .Author}}
<p class="card-text">
<small class="text-muted">来源:{{.Author}}</small>
</p>
{{end}} {{if .Password}}
<p class="card-text">解压密码:<code>{{.Password}}</code></p>
{{end}}
<div class="d-flex justify-content-between align-items-center">
<a href="{{.Download}}" class="btn btn-success" target="_blank"
>下载</a
>
<a href="{{.Url}}" class="btn btn-outline-primary" target="_blank"
>详情页</a
>
</div>
</div>
<div class="card-footer text-muted">
更新时间:{{.UpdatedAt.Format "2006-01-02 15:04:05"}}
</div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}} {{define "scripts"}}
<script src="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.js"></script>
<script>
const swiper = new Swiper(".screenshot-gallery", {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
pagination: {
el: ".swiper-pagination",
clickable: true,
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
</script>
{{end}}

View File

@ -0,0 +1,90 @@
{{template "base" .}} {{define "title"}}GameDB{{end}} {{define "styles"}}
<style>
.game-card {
height: 100%;
transition: transform 0.2s;
text-decoration: none;
color: inherit;
}
.game-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.game-cover {
height: 200px;
object-fit: cover;
}
.game-description {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 992px) {
.col-lg-5-item {
flex: 0 0 20%;
max-width: 20%;
}
}
</style>
{{end}} {{define "content"}}
<!-- Search Section -->
<div class="bg-light py-4">
<div class="container">
<form action="/search" method="GET">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="input-group">
<input
type="text"
class="form-control"
name="q"
placeholder="搜索游戏..."
/>
<button class="btn btn-primary" type="submit">搜索</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Games Grid -->
<div class="container py-4">
<h2 class="mb-4">Month Top</h2>
<div class="row g-4">
{{range .MonthTop}}
<div class="col-12 col-sm-6 col-md-4 col-lg-5-item">
<a href="/game/{{.ID.Hex}}" class="card game-card">
{{if .Cover}}
<img src="{{.Cover}}" class="card-img-top game-cover" alt="{{.Name}}" />
{{else}}
<div
class="card-img-top game-cover bg-secondary d-flex align-items-center justify-content-center"
>
<span class="text-white">暂无图片</span>
</div>
{{end}}
<div class="card-body">
<h5 class="card-title">{{.Name}}</h5>
<p class="card-text game-description">{{.Description}}</p>
{{if .Publishers}}
<div class="publishers mb-2">
<small class="text-muted">发行商:</small>
{{range $index, $publisher := .Publishers}} {{if $index}}, {{end}}
<small class="text-muted">{{$publisher}}</small>
{{end}}
</div>
{{end}}
</div>
</a>
</div>
{{end}}
</div>
</div>
{{end}} {{define "scripts"}} {{end}}

View File

@ -0,0 +1,26 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{template "title" .}}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.css"
/>
{{block "styles" .}}{{end}}
</head>
<body>
{{template "header" .}} {{template "content" .}} {{template "footer" .}}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.js"></script>
{{block "scripts" .}}{{end}}
</body>
</html>
{{end}}

View File

@ -0,0 +1,17 @@
{{define "footer"}}
<!-- Footer -->
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>关于我们</h5>
<p>这是一个提供游戏信息的平台,帮助玩家发现更多优质游戏。</p>
</div>
<div class="col-md-6 text-md-end">
<h5>联系方式</h5>
<a href="https://t.me/bestnite">Telegram</a>
</div>
</div>
</div>
</footer>
{{end}}

View File

@ -0,0 +1,16 @@
{{define "header"}}
<!-- Header -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">游戏库</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
>
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
{{end}}