add: frontend
This commit is contained in:
parent
156b8fbb65
commit
47f6ba2104
@ -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))
|
||||||
}
|
}
|
||||||
|
2
server/templates/404.html
Normal file
2
server/templates/404.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{{template "base" .}} {{define "title"}}Page Not Found{{end}} {{define
|
||||||
|
"styles"}} {{end}} {{define "content"}} Page Not Found {{end}}
|
2
server/templates/500.html
Normal file
2
server/templates/500.html
Normal 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
189
server/templates/game.html
Normal 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}}
|
90
server/templates/index.html
Normal file
90
server/templates/index.html
Normal 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}}
|
26
server/templates/layouts/base.html
Normal file
26
server/templates/layouts/base.html
Normal 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}}
|
17
server/templates/layouts/footer.html
Normal file
17
server/templates/layouts/footer.html
Normal 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}}
|
16
server/templates/layouts/header.html
Normal file
16
server/templates/layouts/header.html
Normal 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}}
|
Loading…
Reference in New Issue
Block a user