增加ico文件

This commit is contained in:
2026-03-05 14:43:00 +08:00
parent 4cab051f9c
commit 6117222625
15 changed files with 288 additions and 28 deletions
+15
View File
@@ -0,0 +1,15 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
}
]
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+5
View File
@@ -225,6 +225,11 @@ func UpdatePost(c *gin.Context) {
updates["published_at"] = &now
}
}
// 如果文章是已发布状态但没有发布时间,设置发布时间
if post.Status == "published" && post.PublishedAt == nil {
now := time.Now()
updates["published_at"] = &now
}
updates["is_top"] = req.IsTop
// 处理标签
+28 -18
View File
@@ -15,7 +15,6 @@ import (
"gorm.io/gorm"
)
// 模板函数
func templateFuncs() template.FuncMap {
return template.FuncMap{
"add": func(a, b int) int {
@@ -30,12 +29,6 @@ func templateFuncs() template.FuncMap {
}
}
// 初始化模板 - 返回 gin 可用的 HTMLRender
func InitTemplates(theme string) (*template.Template, error) {
tmpl, err := template.New("").Funcs(templateFuncs()).ParseGlob("templates/" + theme + "/*.html")
return tmpl, err
}
// 首页
type IndexData struct {
Title string
@@ -61,8 +54,7 @@ func IndexView(c *gin.Context) {
tagID, _ := strconv.Atoi(c.Query("tag"))
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author").
Where("status = ?", "published").
Where("published_at IS NOT NULL")
Where("status = ?", "published")
if categoryID > 0 {
db = db.Where("category_id = ?", categoryID)
@@ -98,7 +90,7 @@ func IndexView(c *gin.Context) {
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
c.HTML(http.StatusOK, "index", data)
}
// 文章详情页
@@ -112,6 +104,8 @@ type PostDetailData struct {
Comments []models.Comment
CommentCount int64
Year int
PrevPost *models.Post // 上一篇
NextPost *models.Post // 下一篇
}
func PostView(c *gin.Context) {
@@ -129,7 +123,7 @@ func PostView(c *gin.Context) {
First(&post).Error
if err != nil {
c.HTML(http.StatusNotFound, "base.html", gin.H{
c.HTML(http.StatusNotFound, "index", gin.H{
"Title": "404 - 页面不存在",
"SiteName": cfg.App.Name,
"Year": time.Now().Year(),
@@ -148,6 +142,16 @@ func PostView(c *gin.Context) {
var commentCount int64
database.DB.Model(&models.Comment{}).Where("post_id = ? AND status = ?", post.ID, "approved").Count(&commentCount)
// 获取上一篇(发布时间更早的文章)
var prevPost models.Post
prevErr := database.DB.Where("status = ? AND published_at < ?", "published", post.PublishedAt).
Order("published_at DESC").First(&prevPost).Error
// 获取下一篇(发布时间更晚的文章)
var nextPost models.Post
nextErr := database.DB.Where("status = ? AND published_at > ?", "published", post.PublishedAt).
Order("published_at ASC").First(&nextPost).Error
data := PostDetailData{
Title: post.Title,
SiteName: cfg.App.Name,
@@ -160,7 +164,15 @@ func PostView(c *gin.Context) {
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
// 只有当查询成功时才赋值
if prevErr == nil {
data.PrevPost = &prevPost
}
if nextErr == nil {
data.NextPost = &nextPost
}
c.HTML(http.StatusOK, "post", data)
}
// 独立页面
@@ -180,9 +192,8 @@ func PageView(c *gin.Context) {
var page models.Page
err := database.DB.Where("slug = ? AND status = ?", slug, "published").First(&page).Error
if err != nil {
c.HTML(http.StatusNotFound, "base.html", gin.H{
c.HTML(http.StatusNotFound, "index", gin.H{
"Title": "404 - 页面不存在",
"SiteName": cfg.App.Name,
"Year": time.Now().Year(),
@@ -204,7 +215,7 @@ func PageView(c *gin.Context) {
Year: time.Now().Year(),
}
c.HTML(http.StatusOK, "base.html", data)
c.HTML(http.StatusOK, "page", data)
}
// 提交评论(表单提交)
@@ -228,14 +239,13 @@ func SubmitComment(c *gin.Context) {
Email: email,
Website: website,
Content: content,
IP: c.ClientIP(),
Status: "pending",
}
if err := database.DB.Create(&comment).Error; err != nil {
c.String(http.StatusInternalServerError, "发表评论失败")
c.String(http.StatusInternalServerError, "提交失败")
return
}
c.Redirect(http.StatusFound, "/post/"+post.Slug+"#comment-"+strconv.Itoa(int(comment.ID)))
c.Redirect(http.StatusFound, "/post/"+post.Slug)
}
+4 -6
View File
@@ -8,6 +8,7 @@ import (
"goblog/database"
"goblog/handlers"
"goblog/middleware"
"goblog/utils"
"github.com/gin-gonic/gin"
)
@@ -32,12 +33,9 @@ func main() {
// 创建路由
r := gin.Default()
// 初始化模板
tmpl, err := handlers.InitTemplates(cfg.App.Theme)
if err != nil {
log.Fatal("模板初始化失败:", err)
}
r.SetHTMLTemplate(tmpl)
// 使用自定义的 TemplateRenderer
htmlRenderer := utils.LoadTemplates(cfg.App.Theme)
r.HTMLRender = htmlRenderer
// 静态文件
r.Static("/static", "./static")
+57 -1
View File
@@ -175,6 +175,7 @@ async function loadPosts() {
<td>${post.published_at ? new Date(post.published_at).toLocaleDateString() : '-'}</td>
<td>
<button class="btn-success" onclick="editPost(${post.id})">编辑</button>
${post.status !== 'published' ? `<button class="btn-primary" onclick="changePostStatus(${post.id}, 'published')">发布</button>` : `<button class="btn-secondary" onclick="changePostStatus(${post.id}, 'draft')">下架</button>`}
<button class="btn-danger" onclick="deletePost(${post.id})">删除</button>
</td>
</tr>
@@ -272,6 +273,29 @@ async function handlePostSubmit(e) {
}
}
// 更改文章状态
async function changePostStatus(id, status) {
const actionText = status === 'published' ? '发布' : '下架';
if (!confirm(`确定要${actionText}这篇文章吗?`)) return;
try {
const res = await fetch(`${API_BASE}/admin/posts/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ status })
});
if (res.ok) {
loadPosts();
} else {
const err = await res.json();
alert(err.error || '操作失败');
}
} catch (err) {
alert('网络错误');
}
}
// 删除文章
async function deletePost(id) {
if (!confirm('确定要删除这篇文章吗?')) return;
@@ -437,14 +461,17 @@ async function handleModalSubmit(e) {
const id = document.getElementById('modal-id').value;
const data = {
name: document.getElementById('modal-name').value,
slug: document.getElementById('modal-slug').value
};
// 页面使用 title,分类和标签使用 name
if (type === 'page') {
data.title = document.getElementById('modal-name').value;
data.content = document.getElementById('modal-content').value;
data.order = parseInt(document.getElementById('modal-order').value) || 0;
data.status = 'published';
} else {
data.name = document.getElementById('modal-name').value;
}
try {
@@ -499,6 +526,35 @@ function editTag(id, name, slug) {
document.getElementById('modal').classList.remove('hidden');
}
// 编辑页面
async function editPage(id) {
try {
const res = await fetch(`${API_BASE}/pages/${id}`, { headers: getHeaders() });
const data = await res.json();
const page = data.data;
document.getElementById('modal-type').value = 'page';
document.getElementById('modal-id').value = id;
document.getElementById('modal-title').textContent = '编辑页面';
document.getElementById('modal-name').value = page.title;
document.getElementById('modal-slug').value = page.slug;
// 添加内容区域
document.querySelector('.modal-extra').innerHTML = `
<label>内容</label>
<textarea id="modal-content" rows="5"></textarea>
<label>排序</label>
<input type="number" id="modal-order" value="0">
`;
document.getElementById('modal-content').value = page.content;
document.getElementById('modal-order').value = page.order;
document.getElementById('modal').classList.remove('hidden');
} catch (err) {
alert('加载页面失败');
}
}
// 删除分类
async function deleteCategory(id) {
if (!confirm('确定要删除这个分类吗?')) return;
+1 -1
View File
@@ -247,6 +247,6 @@
</div>
</div>
<script src="/static/admin/app.js?v=3"></script>
<script src="/static/admin/app.js?v=6"></script>
</body>
</html>
+68
View File
@@ -354,6 +354,65 @@ a:hover {
border-top: 1px solid #eee;
}
/* 文章导航 - 上一篇/下一篇 */
.post-navigation {
display: flex;
justify-content: space-between;
gap: 20px;
margin: 30px 0;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.nav-prev,
.nav-next {
flex: 1;
}
.nav-next {
text-align: right;
}
.nav-link {
display: flex;
flex-direction: column;
padding: 10px;
border-radius: 6px;
transition: background-color 0.2s;
}
.nav-link:hover {
background-color: #f5f5f5;
}
.nav-link.disabled {
color: #999;
cursor: not-allowed;
}
.nav-link.disabled:hover {
background-color: transparent;
}
.nav-label {
font-size: 13px;
color: #999;
margin-bottom: 5px;
}
.nav-title {
font-size: 15px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.nav-link:hover .nav-title {
color: #1890ff;
}
/* 响应式 */
@media (max-width: 768px) {
.site-title {
@@ -373,4 +432,13 @@ a:hover {
display: block;
margin: 5px 0;
}
.post-navigation {
flex-direction: column;
gap: 15px;
}
.nav-next {
text-align: left;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+1
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - {{.SiteName}}</title>
<meta name="description" content="{{.Description}}">
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
+2 -2
View File
@@ -15,9 +15,9 @@
{{if .IsTop}}<span class="top-badge">置顶</span>{{end}}
</h2>
<div class="post-meta">
<span class="post-date">{{.PublishedAt.Format "2006-01-02"}}</span>
<span class="post-date">{{if .PublishedAt}}{{.PublishedAt.Format "2006-01-02"}}{{else}}未发布{{end}}</span>
<span class="post-category">
<a href="/?category={{.CategoryID}}">{{.Category.Name}}</a>
{{if .Category}}<a href="/?category={{.CategoryID}}">{{.Category.Name}}</a>{{else}}未分类{{end}}
</span>
{{if .Tags}}
<span class="post-tags">
+29
View File
@@ -30,6 +30,35 @@
</div>
</article>
<nav class="post-navigation">
<div class="nav-prev">
{{if .PrevPost}}
<a href="/post/{{.PrevPost.Slug}}" class="nav-link">
<span class="nav-label">← 上一篇</span>
<span class="nav-title">{{.PrevPost.Title}}</span>
</a>
{{else}}
<span class="nav-link disabled">
<span class="nav-label">← 上一篇</span>
<span class="nav-title">没有了</span>
</span>
{{end}}
</div>
<div class="nav-next">
{{if .NextPost}}
<a href="/post/{{.NextPost.Slug}}" class="nav-link">
<span class="nav-label">下一篇 →</span>
<span class="nav-title">{{.NextPost.Title}}</span>
</a>
{{else}}
<span class="nav-link disabled">
<span class="nav-label">下一篇 →</span>
<span class="nav-title">没有了</span>
</span>
{{end}}
</div>
</nav>
<section class="comments-section">
<h3>评论 ({{.CommentCount}})</h3>
+78
View File
@@ -0,0 +1,78 @@
package utils
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin/render"
)
// 模板函数
func templateFuncs() template.FuncMap {
return template.FuncMap{
"add": func(a, b int) int {
return a + b
},
"sub": func(a, b int) int {
return a - b
},
"html": func(s string) template.HTML {
return template.HTML(s)
},
}
}
// 自定义 HTMLRender,存储一个 map:页面名 -> *template.Template
type TemplateRenderer struct {
templates map[string]*template.Template
}
func (r *TemplateRenderer) Lookup(name string) *template.Template {
return r.templates[name]
}
func (r *TemplateRenderer) Instance(name string, data any) render.Render {
return &htmlRender{
tmpl: r.Lookup(name),
data: data,
name: name,
}
}
type htmlRender struct {
tmpl *template.Template
data any
name string
}
func (r *htmlRender) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return r.tmpl.ExecuteTemplate(w, "base.html", r.data)
}
func (r *htmlRender) WriteContentType(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
// 初始化多模板
func LoadTemplates(theme string) *TemplateRenderer {
r := &TemplateRenderer{
templates: make(map[string]*template.Template),
}
templateDir := "templates/" + theme
// 为每个页面创建独立的模板组合
pages := []string{"index", "post", "page"}
for _, pageName := range pages {
// 组合 base.html 和具体页面模板
allFiles := []string{
templateDir + "/base.html",
templateDir + "/" + pageName + ".html",
}
tmpl := template.Must(template.New("").Funcs(templateFuncs()).ParseFiles(allFiles...))
r.templates[pageName] = tmpl
}
return r
}