add: frontend
This commit is contained in:
parent
45c9bd3b40
commit
2c969680e0
@ -697,3 +697,30 @@ func GetIGDBPopularGameIDs(popularityType int, offset int, limit int) ([]int, er
|
|||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetIGDBPopularGameIDsCache(popularityType int, offset int, limit int) ([]int, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := fmt.Sprintf("igdb_popular_game_ids:%v:%v:%v", popularityType, offset, limit)
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var data []int
|
||||||
|
if err := json.Unmarshal([]byte(val), &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
} else {
|
||||||
|
data, err := GetIGDBPopularGameIDs(popularityType, offset, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, jsonBytes, 12*time.Hour)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetIGDBPopularGameIDs(popularityType, offset, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,10 @@ package crawler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"pcgamedb/cache"
|
||||||
|
"pcgamedb/config"
|
||||||
"pcgamedb/db"
|
"pcgamedb/db"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -54,18 +57,158 @@ func GetSteam250Top250() ([]*model.GameInfo, error) {
|
|||||||
return GetSteam250(constant.Steam250Top250URL)
|
return GetSteam250(constant.Steam250Top250URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSteam250Top250Cache() ([]*model.GameInfo, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := "steam250_top250"
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var infos []*model.GameInfo
|
||||||
|
err := json.Unmarshal([]byte(val), &infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
} else {
|
||||||
|
infos, err := GetSteam250Top250()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, string(jsonBytes), 12*time.Hour)
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetSteam250Top250()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetSteam250BestOfTheYear() ([]*model.GameInfo, error) {
|
func GetSteam250BestOfTheYear() ([]*model.GameInfo, error) {
|
||||||
return GetSteam250(fmt.Sprintf(constant.Steam250BestOfTheYearURL, time.Now().UTC().Year()))
|
return GetSteam250(fmt.Sprintf(constant.Steam250BestOfTheYearURL, time.Now().UTC().Year()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSteam250BestOfTheYearCache() ([]*model.GameInfo, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := "steam250_best_of_the_year"
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var infos []*model.GameInfo
|
||||||
|
err := json.Unmarshal([]byte(val), &infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
} else {
|
||||||
|
infos, err := GetSteam250BestOfTheYear()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, string(jsonBytes), 12*time.Hour)
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetSteam250BestOfTheYear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetSteam250WeekTop50() ([]*model.GameInfo, error) {
|
func GetSteam250WeekTop50() ([]*model.GameInfo, error) {
|
||||||
return GetSteam250(constant.Steam250WeekTop50URL)
|
return GetSteam250(constant.Steam250WeekTop50URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSteam250WeekTop50Cache() ([]*model.GameInfo, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := "steam250_week_top50"
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var infos []*model.GameInfo
|
||||||
|
err := json.Unmarshal([]byte(val), &infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
} else {
|
||||||
|
infos, err := GetSteam250WeekTop50()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, string(jsonBytes), 12*time.Hour)
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetSteam250WeekTop50()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetSteam250MonthTop50() ([]*model.GameInfo, error) {
|
func GetSteam250MonthTop50() ([]*model.GameInfo, error) {
|
||||||
return GetSteam250(constant.Steam250MonthTop50URL)
|
return GetSteam250(constant.Steam250MonthTop50URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSteam250MonthTop50Cache() ([]*model.GameInfo, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := "steam250_month_top50"
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var infos []*model.GameInfo
|
||||||
|
err := json.Unmarshal([]byte(val), &infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
} else {
|
||||||
|
infos, err := GetSteam250MonthTop50()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, string(jsonBytes), 12*time.Hour)
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetSteam250MonthTop50()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetSteam250MostPlayed() ([]*model.GameInfo, error) {
|
func GetSteam250MostPlayed() ([]*model.GameInfo, error) {
|
||||||
return GetSteam250(constant.Steam250MostPlayedURL)
|
return GetSteam250(constant.Steam250MostPlayedURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSteam250MostPlayedCache() ([]*model.GameInfo, error) {
|
||||||
|
if config.Config.RedisAvaliable {
|
||||||
|
key := "steam250_most_played"
|
||||||
|
val, exist := cache.Get(key)
|
||||||
|
if exist {
|
||||||
|
var infos []*model.GameInfo
|
||||||
|
err := json.Unmarshal([]byte(val), &infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
} else {
|
||||||
|
infos, err := GetSteam250MostPlayed()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(infos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = cache.AddWithExpire(key, string(jsonBytes), 12*time.Hour)
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return GetSteam250MostPlayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -291,6 +291,7 @@ func GetGameItemsByIDs(ids []primitive.ObjectID) ([]*model.GameItem, error) {
|
|||||||
return items, err
|
return items, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchGameItems page start from 1, return (items, totalPage, error)
|
||||||
func SearchGameInfos(name string, page int, pageSize int) ([]*model.GameInfo, int, error) {
|
func SearchGameInfos(name string, page int, pageSize int) ([]*model.GameInfo, int, error) {
|
||||||
var items []*model.GameInfo
|
var items []*model.GameInfo
|
||||||
name = removeNoneAlphaNumeric.ReplaceAllString(name, " ")
|
name = removeNoneAlphaNumeric.ReplaceAllString(name, " ")
|
||||||
|
1
go.mod
1
go.mod
@ -40,6 +40,7 @@ require (
|
|||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||||
|
github.com/gin-contrib/multitemplate v1.0.1 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -112,6 +112,8 @@ github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQ
|
|||||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/multitemplate v1.0.1 h1:Asi8boB7NctSoQzbWDosLObon0cYMP5OM+ihQMjlW5M=
|
||||||
|
github.com/gin-contrib/multitemplate v1.0.1/go.mod h1:uU+PnuKoiEHWqB9Zvco+Kqv9KNrsHi6IZOUUgTctMPA=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
@ -7,8 +7,10 @@ import (
|
|||||||
"pcgamedb/log"
|
"pcgamedb/log"
|
||||||
"pcgamedb/server/handler"
|
"pcgamedb/server/handler"
|
||||||
"pcgamedb/server/middleware"
|
"pcgamedb/server/middleware"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-contrib/multitemplate"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -29,6 +31,9 @@ func initRoute(app *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initFrontend(app *gin.Engine) {
|
func initFrontend(app *gin.Engine) {
|
||||||
|
|
||||||
|
r := multitemplate.NewRenderer()
|
||||||
|
|
||||||
app.Static("/static", "server/static")
|
app.Static("/static", "server/static")
|
||||||
|
|
||||||
layoutFiles, err := filepath.Glob("server/templates/layouts/*.html")
|
layoutFiles, err := filepath.Glob("server/templates/layouts/*.html")
|
||||||
@ -43,16 +48,33 @@ func initFrontend(app *gin.Engine) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LoadHTMLFiles(append(layoutFiles, rootFiles...)...)
|
for _, rootFile := range rootFiles {
|
||||||
|
name := filepath.Base(rootFile)
|
||||||
|
r.AddFromFiles(name, append([]string{rootFile}, layoutFiles...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.HTMLRender = r
|
||||||
|
|
||||||
app.GET("/", func(ctx *gin.Context) {
|
app.GET("/", func(ctx *gin.Context) {
|
||||||
infos, err := crawler.GetSteam250MonthTop50()
|
monthTop, err := crawler.GetSteam250MonthTop50Cache()
|
||||||
|
if err != nil {
|
||||||
|
ctx.HTML(500, "500.html", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mostPlayed, err := crawler.GetSteam250MostPlayedCache()
|
||||||
|
if err != nil {
|
||||||
|
ctx.HTML(500, "500.html", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bestOfTheYear, err := crawler.GetSteam250BestOfTheYearCache()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.HTML(500, "500.html", err)
|
ctx.HTML(500, "500.html", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.HTML(200, "index.html", gin.H{
|
ctx.HTML(200, "index.html", gin.H{
|
||||||
"MonthTop": infos,
|
"MonthTop": monthTop,
|
||||||
|
"MostPlayed": mostPlayed,
|
||||||
|
"BestOfTheYear": bestOfTheYear,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -74,9 +96,48 @@ func initFrontend(app *gin.Engine) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
info.Games = games
|
info.Games = games
|
||||||
//TODO: fix this
|
|
||||||
ctx.HTML(200, "game.html", info)
|
ctx.HTML(200, "game.html", info)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.GET("/search", func(ctx *gin.Context) {
|
||||||
|
key := ctx.Query("key")
|
||||||
|
page := ctx.Query("page")
|
||||||
|
if len(key) < 2 {
|
||||||
|
ctx.HTML(400, "400.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if page == "" {
|
||||||
|
page = "1"
|
||||||
|
}
|
||||||
|
pageInt, err := strconv.Atoi(page)
|
||||||
|
if err != nil {
|
||||||
|
ctx.HTML(400, "400.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
ctx.HTML(400, "400.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
games, totalPage, err := db.SearchGameInfos(key, pageInt, 10)
|
||||||
|
if err != nil {
|
||||||
|
ctx.HTML(500, "500.html", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := gin.H{
|
||||||
|
"Games": games,
|
||||||
|
"TotalPage": totalPage,
|
||||||
|
"CurrentPage": pageInt,
|
||||||
|
"Key": key,
|
||||||
|
}
|
||||||
|
if pageInt > 1 {
|
||||||
|
res["PrevPage"] = pageInt - 1
|
||||||
|
}
|
||||||
|
if pageInt < totalPage {
|
||||||
|
res["NextPage"] = pageInt + 1
|
||||||
|
}
|
||||||
|
ctx.HTML(200, "search.html", res)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func initApi(app *gin.Engine) {
|
func initApi(app *gin.Engine) {
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
{{template "base" .}} {{define "title"}}Page Not Found{{end}} {{define
|
{{template "base" .}}
|
||||||
"styles"}} {{end}} {{define "content"}} Page Not Found {{end}}
|
{{define "title"}}Page Not Found{{end}}
|
||||||
|
{{define "styles"}} {{end}}
|
||||||
|
{{define "content"}} Page Not Found {{end}}
|
@ -1,2 +1,4 @@
|
|||||||
{{template "base" .}} {{define "title"}}Server Error{{end}} {{define "styles"}}
|
{{template "base" .}}
|
||||||
{{end}} {{define "content"}} {{ . }} {{end}}
|
{{define "title"}}Server Error{{end}}
|
||||||
|
{{define "styles"}}{{end}}
|
||||||
|
{{define "content"}} {{ . }} {{end}}
|
@ -1,5 +1,8 @@
|
|||||||
{{template "base" .}} {{define "title"}}{{.Name}} - 游戏详情{{end}} {{define
|
{{template "base" .}}
|
||||||
"styles"}}
|
|
||||||
|
{{define "title"}}{{.Name}} - 游戏详情{{end}}
|
||||||
|
|
||||||
|
{{define "styles"}}
|
||||||
<style>
|
<style>
|
||||||
.game-cover {
|
.game-cover {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
@ -39,22 +42,18 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{{end}} {{define "content"}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
<!-- Game Details -->
|
<!-- Game Details -->
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{{if .Cover}}
|
{{if .Cover}}
|
||||||
<img
|
<img src="{{.Cover}}" class="img-fluid rounded game-cover" alt="{{.Name}}" />
|
||||||
src="{{.Cover}}"
|
|
||||||
class="img-fluid rounded game-cover"
|
|
||||||
alt="{{.Name}}"
|
|
||||||
/>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div
|
<div class="game-cover bg-secondary d-flex align-items-center justify-content-center rounded">
|
||||||
class="game-cover bg-secondary d-flex align-items-center justify-content-center rounded"
|
|
||||||
>
|
|
||||||
<span class="text-white">暂无封面</span>
|
<span class="text-white">暂无封面</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -98,11 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}} {{if .SteamID}}
|
{{end}} {{if .SteamID}}
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a href="https://store.steampowered.com/app/{{.SteamID}}" target="_blank" class="btn btn-primary">
|
||||||
href="https://store.steampowered.com/app/{{.SteamID}}"
|
|
||||||
target="_blank"
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
在 Steam 上查看
|
在 Steam 上查看
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -113,7 +108,6 @@
|
|||||||
<!-- Screenshots -->
|
<!-- Screenshots -->
|
||||||
{{if .Screenshots}}
|
{{if .Screenshots}}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="mb-3">游戏截图</h3>
|
|
||||||
<div class="swiper screenshot-gallery">
|
<div class="swiper screenshot-gallery">
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
{{range .Screenshots}}
|
{{range .Screenshots}}
|
||||||
@ -138,25 +132,32 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card download-card">
|
<div class="card download-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{{.Name}}</h5>
|
<h5 class="card-title">{{.RawName}}</h5>
|
||||||
{{if .Size}}
|
{{if .Size}}
|
||||||
<p class="card-text">
|
<div class="card-text">
|
||||||
<small class="text-muted">文件大小:{{.Size}}</small>
|
<small class="text-muted">文件大小:{{.Size}}</small>
|
||||||
</p>
|
</div>
|
||||||
{{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}}
|
{{end}}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
{{if .Author}}
|
||||||
<a href="{{.Download}}" class="btn btn-success" target="_blank"
|
<div class="card-text">
|
||||||
>下载</a
|
<small class="text-muted">来源:{{.Author}}</small>
|
||||||
>
|
</div>
|
||||||
<a href="{{.Url}}" class="btn btn-outline-primary" target="_blank"
|
{{end}}
|
||||||
>详情页</a
|
{{if .Password}}
|
||||||
>
|
<div class="card-text">
|
||||||
|
解压密码:<code>{{.Password}}</code>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="mt-2 d-flex justify-content-between align-items-center">
|
||||||
|
<!-- <a href="{{.Download}}" class="btn btn-success" target="_blank">下载</a> -->
|
||||||
|
<div class="input-group" style="max-width: 300px;">
|
||||||
|
<input type="text" class="form-control form-control-sm" value="{{.Download}}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||||||
|
onclick="copyToClipboard(this.previousElementSibling)">
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="{{.Url}}" class="btn btn-outline-primary" target="_blank">详情页</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-muted">
|
<div class="card-footer text-muted">
|
||||||
@ -169,7 +170,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}} {{define "scripts"}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "scripts"}}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const swiper = new Swiper(".screenshot-gallery", {
|
const swiper = new Swiper(".screenshot-gallery", {
|
||||||
@ -185,5 +188,26 @@
|
|||||||
prevEl: ".swiper-button-prev",
|
prevEl: ".swiper-button-prev",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function copyToClipboard(input) {
|
||||||
|
input.select();
|
||||||
|
input.setSelectionRange(0, 99999);
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(input.value).then(() => {
|
||||||
|
const button = input.nextElementSibling;
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = '已复制';
|
||||||
|
button.classList.remove('btn-outline-secondary');
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-secondary');
|
||||||
|
}, 1500);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
@ -1,4 +1,8 @@
|
|||||||
{{template "base" .}} {{define "title"}}GameDB{{end}} {{define "styles"}}
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}GameDB{{end}}
|
||||||
|
|
||||||
|
{{define "styles"}}
|
||||||
<style>
|
<style>
|
||||||
.game-card {
|
.game-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -19,6 +23,7 @@
|
|||||||
|
|
||||||
.game-description {
|
.game-description {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
line-clamp: 3;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -32,7 +37,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{{end}} {{define "content"}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
<!-- Search Section -->
|
<!-- Search Section -->
|
||||||
<div class="bg-light py-4">
|
<div class="bg-light py-4">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -40,12 +47,7 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input type="text" class="form-control" name="key" placeholder="搜索游戏..." />
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="q"
|
|
||||||
placeholder="搜索游戏..."
|
|
||||||
/>
|
|
||||||
<button class="btn btn-primary" type="submit">搜索</button>
|
<button class="btn btn-primary" type="submit">搜索</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -64,9 +66,7 @@
|
|||||||
{{if .Cover}}
|
{{if .Cover}}
|
||||||
<img src="{{.Cover}}" class="card-img-top game-cover" alt="{{.Name}}" />
|
<img src="{{.Cover}}" class="card-img-top game-cover" alt="{{.Name}}" />
|
||||||
{{else}}
|
{{else}}
|
||||||
<div
|
<div class="card-img-top game-cover bg-secondary d-flex align-items-center justify-content-center">
|
||||||
class="card-img-top game-cover bg-secondary d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<span class="text-white">暂无图片</span>
|
<span class="text-white">暂无图片</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -87,4 +87,67 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}} {{define "scripts"}} {{end}}
|
|
||||||
|
<div class="container py-4">
|
||||||
|
<h2 class="mb-4">Most Played</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
{{range .MostPlayed}}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="container py-4">
|
||||||
|
<h2 class="mb-4">Best Of The Year</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
{{range .BestOfTheYear}}
|
||||||
|
<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}}
|
@ -16,10 +16,9 @@
|
|||||||
{{block "styles" .}}{{end}}
|
{{block "styles" .}}{{end}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{template "header" .}} {{template "content" .}} {{template "footer" .}}
|
{{template "header" .}} {{block "content" .}}{{end}} {{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/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}}
|
{{block "scripts" .}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -2,15 +2,9 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-dark text-light py-4 mt-5">
|
<footer class="bg-dark text-light py-4 mt-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="col text-center">
|
||||||
<div class="col-md-6">
|
<a href="https://git.nite07.com/nite/pcgamedb" target="_blank">开源仓库</a> | <a href="/api/swagger/index.html">API
|
||||||
<h5>关于我们</h5>
|
文档</a>
|
||||||
<p>这是一个提供游戏信息的平台,帮助玩家发现更多优质游戏。</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 text-md-end">
|
|
||||||
<h5>联系方式</h5>
|
|
||||||
<a href="https://t.me/bestnite">Telegram</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -3,14 +3,13 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="/">游戏库</a>
|
<a class="navbar-brand" href="/">游戏库</a>
|
||||||
<button
|
<div class="navbar-collapse" id="navbarNav">
|
||||||
class="navbar-toggler"
|
<ul class="navbar-nav">
|
||||||
type="button"
|
<li class="nav-item">
|
||||||
data-bs-toggle="collapse"
|
<a class="nav-link" href="https://www.nite07.com" target="_blank">博客</a>
|
||||||
data-bs-target="#navbarNav"
|
</li>
|
||||||
>
|
</ul>
|
||||||
<span class="navbar-toggler-icon"></span>
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
{{end}}
|
96
server/templates/search.html
Normal file
96
server/templates/search.html
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Key}} - Search{{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;
|
||||||
|
line-clamp: 3;
|
||||||
|
-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"}}
|
||||||
|
<!-- page -->
|
||||||
|
<div class="container py-4">
|
||||||
|
<h2 class="mb-4">Search</h2>
|
||||||
|
<small>{{.CurrentPage}}/{{.TotalPage}} pages found for "{{.Key}}"</small>
|
||||||
|
</div>
|
||||||
|
<!-- Games Grid -->
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row g-4">
|
||||||
|
{{range .Games}}
|
||||||
|
<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>
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="container py-4">
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{{if .PrevPage}}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="/search?key={{.Key}}&page={{.PrevPage}}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
{{if .NextPage}}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="/search?key={{.Key}}&page={{.NextPage}}">Next</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "scripts"}} {{end}}
|
Loading…
Reference in New Issue
Block a user