增加ico文件
This commit is contained in:
Vendored
+15
@@ -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
Binary file not shown.
BIN
Binary file not shown.
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user