up
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(curl -s -c /tmp/cookies.txt -L -X POST http://localhost:8099/login -d \"username=admin&password=admin\" -o /dev/null -w \"login final HTTP %{http_code}\\\\n\")",
|
||||
"Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o /tmp/dash.html -w \"HTTP %{http_code}\\\\n\")",
|
||||
"Read(//tmp/**)",
|
||||
"Bash(sed -n 's/.*\\\\\\(text-3xl font-bold text-gray-900 mt-1\">1\\\\n\\\\\\).*//p' /tmp/dash.html)",
|
||||
"Bash(perl -0777 -ne 'while\\(/dash_posts.*?text-3xl[^>]*>\\(.*?\\)<\\\\/p>/sg\\){print \"POSTS BLOCK: $1\\\\n\"}' /tmp/dash.html)",
|
||||
"Bash(python -c ' *)",
|
||||
"Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o dash.html)",
|
||||
"Bash(rm -f dash.html)",
|
||||
"Bash(curl -s http://localhost:8099/article/123 -o detail.html -w \"detail HTTP %{http_code}\\\\n\")",
|
||||
"Bash(rm -f detail.html)",
|
||||
"Bash(cp /tmp/cfg_backup.yaml win/etc/blog_go/config.yaml)",
|
||||
"Bash(pkill -f blogtest.exe)",
|
||||
"Bash(pkill -f \"blog_go.exe\")",
|
||||
"Bash(cd c:/Users/wuwen/Documents/project/go_blog && rm -f /tmp/blogtest.exe && git status --short && echo \"--- diff stat ---\" && git diff --stat)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"\\tmp"
|
||||
]
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -4,16 +4,27 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go_blog/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminDashboard renders the protected admin dashboard.
|
||||
func AdminDashboard() gin.HandlerFunc {
|
||||
func AdminDashboard(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
username, _ := c.Get("username")
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["dash_page_title"]
|
||||
data["Username"] = username
|
||||
|
||||
// Article counts: total and published.
|
||||
var postCount, publishedCount int64
|
||||
db.Model(&models.Article{}).Count(&postCount)
|
||||
db.Model(&models.Article{}).Where("status = ?", models.ArticlePublished).Count(&publishedCount)
|
||||
data["PostCount"] = postCount
|
||||
data["PublishedCount"] = publishedCount
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard", data)
|
||||
}
|
||||
}
|
||||
|
||||
+62
-2
@@ -2,16 +2,76 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go_blog/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HomePage renders the public home page.
|
||||
func HomePage() gin.HandlerFunc {
|
||||
// publishedArticleOrder is the ordering used to list published articles:
|
||||
// pinned first, then by most recent publish/creation time.
|
||||
const publishedArticleOrder = "is_top DESC, published_at DESC, created_at DESC"
|
||||
|
||||
// HomePage renders the public home page with the latest published articles.
|
||||
func HomePage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["page_home"]
|
||||
|
||||
var articles []models.Article
|
||||
db.Where("status = ?", models.ArticlePublished).
|
||||
Order(publishedArticleOrder).
|
||||
Limit(10).
|
||||
Find(&articles)
|
||||
data["Articles"] = articles
|
||||
|
||||
c.HTML(http.StatusOK, "home", data)
|
||||
}
|
||||
}
|
||||
|
||||
// ArticleDetail renders a single published article by its slug.
|
||||
func ArticleDetail(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
slug := c.Param("slug")
|
||||
|
||||
var article models.Article
|
||||
err := db.Where("slug = ? AND status = ?", slug, models.ArticlePublished).
|
||||
Preload("Author").
|
||||
First(&article).Error
|
||||
if err != nil {
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["article_not_found"]
|
||||
c.HTML(http.StatusNotFound, "article_not_found", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Increment view count; ignore errors so a view never breaks the page.
|
||||
db.Model(&models.Article{}).Where("id = ?", article.ID).
|
||||
UpdateColumn("view_count", gorm.Expr("view_count + 1"))
|
||||
|
||||
authorName := article.Author.Username
|
||||
if article.Author.DisplayName != "" {
|
||||
authorName = article.Author.DisplayName
|
||||
}
|
||||
|
||||
data := DefaultData(c)
|
||||
data["Title"] = article.Title
|
||||
data["Article"] = article
|
||||
data["AuthorName"] = authorName
|
||||
data["PublishedAt"] = formatPublishTime(article.PublishedAt)
|
||||
c.HTML(http.StatusOK, "article", data)
|
||||
}
|
||||
}
|
||||
|
||||
// formatPublishTime returns the publication time as a readable string, falling
|
||||
// back to the creation time when the publish timestamp is unset.
|
||||
func formatPublishTime(publishedAt *time.Time) string {
|
||||
if publishedAt != nil {
|
||||
return publishedAt.Format("2006-01-02 15:04")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ var translations = map[Lang]map[string]string{
|
||||
"home_sign_in": "Sign In",
|
||||
"home_to_dash": "Dashboard",
|
||||
"home_latest": "Latest Posts",
|
||||
"home_read_more": "Read more",
|
||||
"home_post1_tit": "Getting Started",
|
||||
"home_post1_dsc": "Welcome to your new blog! This is a placeholder post. Start writing to see your content here.",
|
||||
"home_post2_tit": "Hello World",
|
||||
@@ -52,6 +53,7 @@ var translations = map[Lang]map[string]string{
|
||||
"dash_posts": "Posts",
|
||||
"dash_pages": "Pages",
|
||||
"dash_users": "Users",
|
||||
"dash_published": "Published",
|
||||
"dash_mgmt_title": "Blog Management",
|
||||
"dash_mgmt_desc": "Post management, categories, and settings will appear here in future updates.",
|
||||
"dash_new_article": "New Article",
|
||||
@@ -114,6 +116,8 @@ var translations = map[Lang]map[string]string{
|
||||
"article_new": "New Article",
|
||||
"article_title_required": "Title is required.",
|
||||
"article_content_required": "Content is required.",
|
||||
"article_back_home": "Back to Home",
|
||||
"article_not_found": "Article not found.",
|
||||
},
|
||||
ZH: {
|
||||
// 导航
|
||||
@@ -129,6 +133,7 @@ var translations = map[Lang]map[string]string{
|
||||
"home_sign_in": "登录",
|
||||
"home_to_dash": "后台管理",
|
||||
"home_latest": "最新文章",
|
||||
"home_read_more": "阅读全文",
|
||||
"home_post1_tit": "入门指南",
|
||||
"home_post1_dsc": "欢迎来到你的新博客!这是一篇占位文章。开始写作后,你的内容将显示在这里。",
|
||||
"home_post2_tit": "你好,世界",
|
||||
@@ -153,6 +158,7 @@ var translations = map[Lang]map[string]string{
|
||||
"dash_posts": "文章",
|
||||
"dash_pages": "页面",
|
||||
"dash_users": "用户",
|
||||
"dash_published": "已发布",
|
||||
"dash_mgmt_title": "博客管理",
|
||||
"dash_mgmt_desc": "文章管理、分类和设置将在后续版本中添加。",
|
||||
"dash_new_article": "新建文章",
|
||||
@@ -213,6 +219,8 @@ var translations = map[Lang]map[string]string{
|
||||
"article_new": "新建文章",
|
||||
"article_title_required": "标题不能为空。",
|
||||
"article_content_required": "正文不能为空。",
|
||||
"article_back_home": "返回首页",
|
||||
"article_not_found": "文章不存在。",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,17 @@ func main() {
|
||||
router.Use(middleware.SetUserContext(db))
|
||||
|
||||
// 8. Register routes.
|
||||
router.GET("/", handlers.HomePage())
|
||||
router.GET("/", handlers.HomePage(db))
|
||||
router.GET("/login", handlers.LoginPage())
|
||||
router.POST("/login", handlers.Login(db))
|
||||
router.POST("/logout", handlers.Logout())
|
||||
router.GET("/article/:slug", handlers.ArticleDetail(db))
|
||||
|
||||
// Protected admin routes.
|
||||
admin := router.Group("/admin")
|
||||
admin.Use(middleware.AuthRequired())
|
||||
{
|
||||
admin.GET("", handlers.AdminDashboard())
|
||||
admin.GET("", handlers.AdminDashboard(db))
|
||||
admin.GET("/articles/new", handlers.ArticleCreatePage(db))
|
||||
admin.POST("/articles/new", handlers.ArticleCreate(db))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<p class="text-sm text-gray-500 uppercase tracking-wider">{{index .Tr "dash_posts"}}</p>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-1">0</p>
|
||||
<p class="text-3xl font-bold text-gray-900 mt-1">{{.PostCount}}
|
||||
<span class="text-sm font-normal text-gray-400">({{.PublishedCount}} {{index .Tr "dash_published"}})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<p class="text-sm text-gray-500 uppercase tracking-wider">{{index .Tr "dash_pages"}}</p>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{{define "article_not_found"}}
|
||||
{{template "header" .}}
|
||||
<section class="max-w-3xl mx-auto px-4 py-24 text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-3">{{index .Tr "article_not_found"}}</h1>
|
||||
<a href="/" class="inline-block mt-4 bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
|
||||
{{index .Tr "article_back_home"}}
|
||||
</a>
|
||||
</section>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,39 @@
|
||||
{{define "article"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<section class="max-w-3xl mx-auto px-4 py-12">
|
||||
<a href="/" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-blue-600 transition-colors mb-6">
|
||||
← {{index .Tr "article_back_home"}}
|
||||
</a>
|
||||
|
||||
<article>
|
||||
<h1 class="text-4xl font-extrabold text-gray-900 mb-3">{{.Article.Title}}</h1>
|
||||
<div class="flex items-center gap-3 text-sm text-gray-500 mb-8">
|
||||
{{if .Article.Author.Avatar}}
|
||||
<img src="/uploads/avatars/{{.Article.Author.Avatar}}" alt="avatar" class="w-7 h-7 rounded-full object-cover">
|
||||
{{end}}
|
||||
<span class="font-medium text-gray-700">{{.AuthorName}}</span>
|
||||
{{if .PublishedAt}}<span>·</span><span>{{.PublishedAt}}</span>{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Article.Cover}}
|
||||
<div class="rounded-xl overflow-hidden mb-8">
|
||||
<img src="{{.Article.Cover}}" alt="{{.Article.Title}}" class="w-full max-h-96 object-cover">
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div id="articleBody" class="prose max-w-none text-gray-800"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// {{.Article.Content}} is emitted by html/template as a JSON-encoded JS
|
||||
// string, safely handling quotes, newlines, and HTML special characters.
|
||||
var md = {{.Article.Content}};
|
||||
document.getElementById('articleBody').innerHTML = marked.parse(md);
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
+19
-12
@@ -20,18 +20,25 @@
|
||||
<section class="max-w-5xl mx-auto px-4 py-16">
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-8 text-center">{{index .Tr "home_latest"}}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">{{index .Tr "home_post1_tit"}}</h3>
|
||||
<p class="text-gray-500 text-sm">{{index .Tr "home_post1_dsc"}}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">{{index .Tr "home_post2_tit"}}</h3>
|
||||
<p class="text-gray-500 text-sm">{{index .Tr "home_post2_dsc"}}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">{{index .Tr "home_post3_tit"}}</h3>
|
||||
<p class="text-gray-500 text-sm">{{index .Tr "home_post3_dsc"}}</p>
|
||||
</div>
|
||||
{{range .Articles}}
|
||||
<a href="/article/{{.Slug}}"
|
||||
class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow flex flex-col">
|
||||
{{if .Cover}}
|
||||
<div class="aspect-video w-full overflow-hidden bg-gray-100">
|
||||
<img src="{{.Cover}}" alt="{{.Title}}" class="w-full h-full object-cover">
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="p-6 flex flex-col flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">{{.Title}}
|
||||
{{if .IsTop}}<span class="align-middle text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded ml-1">{{index $.Tr "article_is_top"}}</span>{{end}}
|
||||
</h3>
|
||||
{{if .Summary}}<p class="text-gray-500 text-sm line-clamp-2">{{.Summary}}</p>{{end}}
|
||||
<span class="mt-4 text-sm text-blue-600 font-medium">{{index $.Tr "home_read_more"}} →</span>
|
||||
</div>
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="col-span-full text-center text-gray-500 py-12">{{index .Tr "home_no_posts"}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{template "footer" .}}
|
||||
|
||||
Reference in New Issue
Block a user