feat: 文章 CRUD 接口 JSON 化——/api/admin|my/articles
- article.go:articleForm 加 json tags,新增 parseArticleFormJSON(绑定+
去白);ArticleCreate/ArticleUpdate/ArticleDelete 增加 redirectPath 参数,
校验失败改 APIError 400、DB 错误 500,成功 {ok,redirect}(原 renderForm
分支移除);id 改用 parseUintParam 数值化
- my_articles.go:MyArticleCreate 复用 ArticleCreate(db, /my/articles);
MyArticleUpdate/MyArticleDelete 保留 author_id 所有权约束(404
article_not_found);my 更新不触碰标签(表单无该字段,与旧行为一致)
- main.go:admin/my 文章旧 POST 路由移除,改 POST/PUT/DELETE
/api/admin/articles[/:id] 与 /api/my/articles[/:id]
- 模板:article_create.html/my_article_form.html 表单改 blogAPI 提交
(easyMDE.value() 同步正文、editor e.submitter 分流草稿/发布),错误内联
articleError/myArticleError;article_list.html/my_articles.html 删除改
blogDelete 委托(base.html 新公共函数,DELETE + reload 保筛选状态)
- main_test 冒烟补文章 CRUD 断言;go build/vet/test 全绿
This commit is contained in:
9 files changed
+181
-99
No files matched your search
+74
-62
@@ -56,7 +56,7 @@ func generateSlug(title string) string {
|
||||
}
|
||||
|
||||
// fallbackSlug 在由标题派生的 slug 为空时返回非空 slug
|
||||
//(例如标题只包含标点/空白,或全部被剔除)。可用时使用文章 ID,
|
||||
// (例如标题只包含标点/空白,或全部被剔除)。可用时使用文章 ID,
|
||||
// 否则使用短令牌。
|
||||
func fallbackSlug(id uint) string {
|
||||
if id > 0 {
|
||||
@@ -65,41 +65,44 @@ func fallbackSlug(id uint) string {
|
||||
return "post-" + randomToken()[:8]
|
||||
}
|
||||
|
||||
// articleForm 保存解析后的文章表单字段,由创建和编辑处理器
|
||||
// 及其校验错误回填路径共用。
|
||||
// articleForm 是文章创建/更新接口的 JSON 请求体。
|
||||
// Action/TitleText/ArticleID 仅由页面渲染路径使用,不参与 JSON 绑定。
|
||||
type articleForm struct {
|
||||
Title string
|
||||
Slug string
|
||||
Summary string
|
||||
Content string
|
||||
Cover string
|
||||
StatusStr string
|
||||
IsTop bool
|
||||
PublishedAt string // datetime-local 格式:"2006-01-02T15:04"
|
||||
Tags string // 逗号分隔的标签名
|
||||
Action string // 表单提交 URL
|
||||
TitleText string // 页面标题文字(创建还是编辑)
|
||||
ArticleID uint // 已有文章 ID(编辑页面);创建时为 0
|
||||
SessionToken string // 待处理附件的归属令牌(创建页面)
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Summary string `json:"summary"`
|
||||
Content string `json:"content"`
|
||||
Cover string `json:"cover"`
|
||||
StatusStr string `json:"status"` // "0" 草稿、"1" 已发布
|
||||
IsTop bool `json:"is_top"`
|
||||
PublishedAt string `json:"published_at"` // datetime-local:"2006-01-02T15:04"
|
||||
Tags string `json:"tags"` // 逗号分隔的标签名
|
||||
SessionToken string `json:"session_token"`
|
||||
Action string // 表单提交 URL(页面渲染用)
|
||||
TitleText string // 页面标题文字(创建还是编辑)
|
||||
ArticleID uint // 已有文章 ID(编辑页面);创建时为 0
|
||||
}
|
||||
|
||||
// parseArticleForm 从请求中读取并去除空白后的文章表单字段。
|
||||
func parseArticleForm(c *gin.Context) articleForm {
|
||||
return articleForm{
|
||||
Title: strings.TrimSpace(c.PostForm("title")),
|
||||
Slug: strings.TrimSpace(c.PostForm("slug")),
|
||||
Summary: strings.TrimSpace(c.PostForm("summary")),
|
||||
Content: strings.TrimSpace(c.PostForm("content")),
|
||||
Cover: strings.TrimSpace(c.PostForm("cover")),
|
||||
StatusStr: c.PostForm("status"),
|
||||
IsTop: c.PostForm("is_top") == "1",
|
||||
PublishedAt: strings.TrimSpace(c.PostForm("published_at")),
|
||||
Tags: strings.TrimSpace(c.PostForm("tags")),
|
||||
// parseArticleFormJSON 绑定 JSON 请求体并去除空白后的文章字段。
|
||||
// 绑定失败时已写入 400 响应并返回 ok=false。
|
||||
func parseArticleFormJSON(c *gin.Context) (articleForm, bool) {
|
||||
var f articleForm
|
||||
if !bindJSON(c, &f) {
|
||||
return f, false
|
||||
}
|
||||
f.Title = strings.TrimSpace(f.Title)
|
||||
f.Slug = strings.TrimSpace(f.Slug)
|
||||
f.Summary = strings.TrimSpace(f.Summary)
|
||||
f.Content = strings.TrimSpace(f.Content)
|
||||
f.Cover = strings.TrimSpace(f.Cover)
|
||||
f.PublishedAt = strings.TrimSpace(f.PublishedAt)
|
||||
f.Tags = strings.TrimSpace(f.Tags)
|
||||
f.SessionToken = strings.TrimSpace(f.SessionToken)
|
||||
return f, true
|
||||
}
|
||||
|
||||
// applyFormToData 将表单字段值写入模板数据映射,使渲染时表单被重新填充
|
||||
//(初次加载或校验错误)。
|
||||
// (初次加载或校验错误)。
|
||||
func applyFormToData(data gin.H, f articleForm) {
|
||||
data["FormTitle"] = f.Title
|
||||
data["FormSlug"] = f.Slug
|
||||
@@ -261,22 +264,22 @@ func ArticleCreatePage(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ArticleCreate 处理创建新文章的 POST 请求。
|
||||
func ArticleCreate(db *gorm.DB) gin.HandlerFunc {
|
||||
// ArticleCreate 处理创建新文章的 POST 请求(admin 与 my 共用)。
|
||||
// redirectPath 是成功跳转目标(admin 用 /admin,普通用户用 /my/articles)。
|
||||
func ArticleCreate(db *gorm.DB, redirectPath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
f := parseArticleForm(c)
|
||||
f.Action = "/admin/articles/new"
|
||||
f.TitleText = tr["article_create_title"]
|
||||
f.SessionToken = strings.TrimSpace(c.PostForm("session_token"))
|
||||
f, ok := parseArticleFormJSON(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验必填字段。
|
||||
if f.Title == "" {
|
||||
renderArticleForm(c, db, f, tr["article_title_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_title_required")
|
||||
return
|
||||
}
|
||||
if f.Content == "" {
|
||||
renderArticleForm(c, db, f, tr["article_content_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_content_required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -294,7 +297,7 @@ func ArticleCreate(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
authorID, ok := sessionAuthorID(c)
|
||||
if !ok {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
APIError(c, http.StatusUnauthorized, "api_unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -321,7 +324,7 @@ func ArticleCreate(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if err := db.Create(&article).Error; err != nil {
|
||||
renderArticleForm(c, db, f, tr["article_error"])
|
||||
APIError(c, http.StatusInternalServerError, "article_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -345,8 +348,8 @@ func ArticleCreate(db *gorm.DB) gin.HandlerFunc {
|
||||
_ = BindPendingAttachments(db, f.SessionToken, article.ID)
|
||||
}
|
||||
|
||||
// 成功:重定向到管理后台。
|
||||
c.Redirect(http.StatusFound, "/admin")
|
||||
// 成功:跳转到文章列表。
|
||||
APIOK(c, redirectPath, gin.H{"article_id": article.ID})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,28 +398,33 @@ func ArticleEditPage(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ArticleUpdate 处理更新现有文章的 POST 请求。
|
||||
func ArticleUpdate(db *gorm.DB) gin.HandlerFunc {
|
||||
// ArticleUpdate 处理更新现有文章的请求(admin 与 my 共用)。
|
||||
// redirectPath 是成功跳转目标(admin 用 /admin/articles,普通用户用 /my/articles)。
|
||||
func ArticleUpdate(db *gorm.DB, redirectPath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
id := c.Param("id")
|
||||
|
||||
var article models.Article
|
||||
if err := db.First(&article, "id = ?", id).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/admin/articles")
|
||||
id := parseUintParam(c, "id")
|
||||
if id == 0 {
|
||||
APIError(c, http.StatusBadRequest, "api_invalid_request")
|
||||
return
|
||||
}
|
||||
|
||||
f := parseArticleForm(c)
|
||||
f.Action = "/admin/articles/" + id + "/edit"
|
||||
f.TitleText = tr["article_edit_title"]
|
||||
var article models.Article
|
||||
if err := db.First(&article, id).Error; err != nil {
|
||||
APIError(c, http.StatusNotFound, "article_not_found")
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := parseArticleFormJSON(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if f.Title == "" {
|
||||
renderArticleForm(c, db, f, tr["article_title_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_title_required")
|
||||
return
|
||||
}
|
||||
if f.Content == "" {
|
||||
renderArticleForm(c, db, f, tr["article_content_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_content_required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,7 +464,7 @@ func ArticleUpdate(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if err := db.Model(&article).Updates(updates).Error; err != nil {
|
||||
renderArticleForm(c, db, f, tr["article_error"])
|
||||
APIError(c, http.StatusInternalServerError, "article_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -466,15 +474,19 @@ func ArticleUpdate(db *gorm.DB) gin.HandlerFunc {
|
||||
// 记录错误但不影响文章更新
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/admin/articles")
|
||||
APIOK(c, redirectPath, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ArticleDelete 软删除文章(GORM 填充 DeletedAt)并重定向回管理列表。
|
||||
func ArticleDelete(db *gorm.DB) gin.HandlerFunc {
|
||||
// ArticleDelete 软删除文章(GORM 填充 DeletedAt)。
|
||||
func ArticleDelete(db *gorm.DB, redirectPath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
db.Delete(&models.Article{}, "id = ?", id)
|
||||
c.Redirect(http.StatusFound, "/admin/articles")
|
||||
id := parseUintParam(c, "id")
|
||||
if id == 0 {
|
||||
APIError(c, http.StatusBadRequest, "api_invalid_request")
|
||||
return
|
||||
}
|
||||
db.Delete(&models.Article{}, id)
|
||||
APIOK(c, redirectPath, nil)
|
||||
}
|
||||
}
|
||||
+23
-15
@@ -48,7 +48,7 @@ func MyArticleCreatePage(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
// MyArticleCreate 处理普通用户创建新文章的 POST 请求。
|
||||
func MyArticleCreate(db *gorm.DB) gin.HandlerFunc {
|
||||
return ArticleCreate(db) // 复用相同的逻辑
|
||||
return ArticleCreate(db, "/my/articles") // 复用创建逻辑,跳转到 my 列表
|
||||
}
|
||||
|
||||
// MyArticleEditPage 为已登录用户自己的文章渲染编辑表单。
|
||||
@@ -81,31 +81,34 @@ func MyArticleEditPage(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// MyArticleUpdate 处理更新用户自己文章的 POST 请求。
|
||||
// MyArticleUpdate 处理更新用户自己文章的请求(带 author_id 所有权约束)。
|
||||
func MyArticleUpdate(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
id := c.Param("id")
|
||||
id := parseUintParam(c, "id")
|
||||
if id == 0 {
|
||||
APIError(c, http.StatusBadRequest, "api_invalid_request")
|
||||
return
|
||||
}
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
|
||||
var article models.Article
|
||||
if err := db.First(&article, "id = ? AND author_id = ?", id, userID).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/my/articles")
|
||||
APIError(c, http.StatusNotFound, "article_not_found")
|
||||
return
|
||||
}
|
||||
|
||||
f := parseArticleForm(c)
|
||||
f.Action = "/my/articles/" + id + "/edit"
|
||||
f.TitleText = tr["article_edit_title"]
|
||||
f.ArticleID = article.ID
|
||||
f, ok := parseArticleFormJSON(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if f.Title == "" {
|
||||
renderMyArticleForm(c, db, f, tr["article_title_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_title_required")
|
||||
return
|
||||
}
|
||||
if f.Content == "" {
|
||||
renderMyArticleForm(c, db, f, tr["article_content_required"])
|
||||
APIError(c, http.StatusBadRequest, "article_content_required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -145,23 +148,28 @@ func MyArticleUpdate(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if err := db.Model(&article).Updates(updates).Error; err != nil {
|
||||
renderMyArticleForm(c, db, f, tr["article_error"])
|
||||
APIError(c, http.StatusInternalServerError, "article_error")
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/my/articles")
|
||||
// 注意:my 表单无 tags 字段,此处不触碰标签(与旧行为一致)。
|
||||
APIOK(c, "/my/articles", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MyArticleDelete 软删除用户自己的文章。
|
||||
func MyArticleDelete(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
id := parseUintParam(c, "id")
|
||||
if id == 0 {
|
||||
APIError(c, http.StatusBadRequest, "api_invalid_request")
|
||||
return
|
||||
}
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
|
||||
db.Where("id = ? AND author_id = ?", id, userID).Delete(&models.Article{})
|
||||
c.Redirect(http.StatusFound, "/my/articles")
|
||||
APIOK(c, "/my/articles", nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,10 +182,15 @@ func registerRoutes(router *gin.Engine, cfg *config.Config, db *gorm.DB, loginLi
|
||||
admin.GET("", handlers.AdminDashboard(db))
|
||||
admin.GET("/articles", handlers.ArticleListPage(db))
|
||||
admin.GET("/articles/new", handlers.ArticleCreatePage(db))
|
||||
admin.POST("/articles/new", handlers.ArticleCreate(db))
|
||||
admin.GET("/articles/:id/edit", handlers.ArticleEditPage(db))
|
||||
admin.POST("/articles/:id/edit", handlers.ArticleUpdate(db))
|
||||
admin.POST("/articles/:id/delete", handlers.ArticleDelete(db))
|
||||
}
|
||||
|
||||
adminArticleAPI := router.Group("/api/admin/articles")
|
||||
adminArticleAPI.Use(middleware.AuthRequired(db), middleware.AdminRequired(db))
|
||||
{
|
||||
adminArticleAPI.POST("", handlers.ArticleCreate(db, "/admin"))
|
||||
adminArticleAPI.PUT("/:id", handlers.ArticleUpdate(db, "/admin/articles"))
|
||||
adminArticleAPI.DELETE("/:id", handlers.ArticleDelete(db, "/admin/articles"))
|
||||
}
|
||||
|
||||
// 受保护的后台评论管理路由(仅管理员角色)。
|
||||
@@ -270,16 +275,16 @@ func registerRoutes(router *gin.Engine, cfg *config.Config, db *gorm.DB, loginLi
|
||||
{
|
||||
myArticles.GET("/articles", handlers.MyArticlesPage(db))
|
||||
myArticles.GET("/articles/new", handlers.MyArticleCreatePage(db))
|
||||
myArticles.POST("/articles/new", handlers.MyArticleCreate(db))
|
||||
myArticles.GET("/articles/:id/edit", handlers.MyArticleEditPage(db))
|
||||
myArticles.POST("/articles/:id/edit", handlers.MyArticleUpdate(db))
|
||||
myArticles.POST("/articles/:id/delete", handlers.MyArticleDelete(db))
|
||||
}
|
||||
|
||||
// 用户文章的受保护附件 API(仅登录用户)。
|
||||
// 用户文章的受保护 API(仅登录用户,含 attachments 静态段与 :id 参数段)。
|
||||
myAPI := router.Group("/api/my/articles")
|
||||
myAPI.Use(middleware.AuthRequired(db))
|
||||
{
|
||||
myAPI.POST("", handlers.MyArticleCreate(db))
|
||||
myAPI.PUT("/:id", handlers.MyArticleUpdate(db))
|
||||
myAPI.DELETE("/:id", handlers.MyArticleDelete(db))
|
||||
myAPI.POST("/attachments", handlers.UploadAttachment(db, cfg.Path))
|
||||
myAPI.DELETE("/attachments/:id", handlers.DeleteAttachment(db, cfg.Path))
|
||||
myAPI.GET("/:id/attachments", handlers.ListAttachments(db))
|
||||
|
||||
@@ -151,6 +151,13 @@ func TestRegisterRoutesSmoke(t *testing.T) {
|
||||
"POST /api/admin/comments/:id/approve": "",
|
||||
"POST /api/admin/comments/:id/reject": "",
|
||||
"POST /api/admin/comments/:id/delete": "",
|
||||
// 文章 CRUD API。
|
||||
"POST /api/admin/articles": "",
|
||||
"PUT /api/admin/articles/:id": "",
|
||||
"DELETE /api/admin/articles/:id": "",
|
||||
"POST /api/my/articles": "",
|
||||
"PUT /api/my/articles/:id": "",
|
||||
"DELETE /api/my/articles/:id": "",
|
||||
// 搬移的附件/头像端点。
|
||||
"POST /api/admin/articles/attachments": "",
|
||||
"DELETE /api/admin/articles/attachments/:id": "",
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
<section class="max-w-3xl mx-auto px-4 py-10">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-8">{{.FormTitleText}}</h2>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
<div id="articleError" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm {{if not .Error}}hidden{{end}}">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form action="{{.FormAction}}" method="post" class="space-y-6">
|
||||
<form id="articleForm" action="{{.FormAction}}" method="post" class="space-y-6">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<!-- Hidden: attachment ownership (token on create, id on edit) -->
|
||||
<input type="hidden" name="session_token" value="{{.SessionToken}}">
|
||||
@@ -256,6 +254,25 @@ var easyMDE = new EasyMDE({
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// ---- Article form submit(JSON API;草稿/发布按钮由 e.submitter 分流) ----
|
||||
(function () {
|
||||
var form = document.getElementById('articleForm');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var btn = e.submitter || null;
|
||||
// EasyMDE 隐藏了 textarea:先同步编辑器内容再提交。
|
||||
var ta = document.getElementById('articleContent');
|
||||
if (ta && easyMDE) { ta.value = easyMDE.value(); }
|
||||
var method = articleID ? 'PUT' : 'POST';
|
||||
var url = articleID ? '/api/admin/articles/' + articleID : '/api/admin/articles';
|
||||
blogAPI(method, url, blogForm(form, btn)).then(function (r) {
|
||||
if (r.ok) { window.location.href = r.redirect || '/admin'; }
|
||||
else { blogShowError('articleError', r.error || 'Failed to save article.'); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{end}}
|
||||
@@ -40,9 +40,10 @@
|
||||
<td class="px-4 py-3 text-sm text-right whitespace-nowrap">
|
||||
<a href="/admin/articles/{{.ID}}/edit"
|
||||
class="text-blue-600 hover:text-blue-800 font-medium mr-3">{{index $.Tr "article_edit"}}</a>
|
||||
<form action="/admin/articles/{{.ID}}/delete" method="post" class="inline"
|
||||
onsubmit="return confirm('{{index $.Tr "article_delete_confirm"}}');">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<form class="inline blog-delete-form"
|
||||
data-url="/api/admin/articles/{{.ID}}"
|
||||
data-confirm="{{index $.Tr "article_delete_confirm"}}"
|
||||
onsubmit="return blogDelete(this)">
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "article_delete"}}</button>
|
||||
</form>
|
||||
|
||||
@@ -235,6 +235,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 列表删除提交(onsubmit = "return blogDelete(this)"):
|
||||
// DELETE data-url 指向的 JSON 接口,成功后 reload(保留筛选状态)。
|
||||
window.blogDelete = function (form) {
|
||||
var url = form.getAttribute('data-url');
|
||||
var confirmText = form.getAttribute('data-confirm');
|
||||
if (confirmText && !confirm(confirmText)) return false;
|
||||
blogAPI('DELETE', url).then(function (r) {
|
||||
if (r.ok) { window.location.reload(); }
|
||||
else { alert(r.error || 'Failed'); }
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
function toggleDropdown() {
|
||||
var menu = document.getElementById('dropdownMenu');
|
||||
menu.classList.toggle('hidden');
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
<h2 class="text-3xl font-bold text-gray-900">{{.FormTitleText}}</h2>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
||||
<div id="myArticleError" class="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg {{if not .Error}}hidden{{end}}">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form action="{{.FormAction}}" method="post" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<form id="myArticleForm" action="{{.FormAction}}" method="post" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
{{if .SessionToken}}
|
||||
<input type="hidden" name="session_token" value="{{.SessionToken}}">
|
||||
@@ -90,8 +88,9 @@
|
||||
</section>
|
||||
|
||||
<script>
|
||||
var myEasyMDE = null;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var easyMDE = new EasyMDE({
|
||||
myEasyMDE = new EasyMDE({
|
||||
element: document.getElementById('content'),
|
||||
spellChecker: false,
|
||||
status: false,
|
||||
@@ -102,6 +101,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
"link", "image", "|", "preview", "side-by-side", "fullscreen", "|", "guide"]
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Form submit(JSON API) ----
|
||||
(function () {
|
||||
var form = document.getElementById('myArticleForm');
|
||||
if (!form) return;
|
||||
var articleId = {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }};
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var btn = e.submitter || null;
|
||||
var ta = document.getElementById('content');
|
||||
if (ta && myEasyMDE) { ta.value = myEasyMDE.value(); }
|
||||
var method = articleId ? 'PUT' : 'POST';
|
||||
var url = articleId ? '/api/my/articles/' + articleId : '/api/my/articles';
|
||||
blogAPI(method, url, blogForm(form, btn)).then(function (r) {
|
||||
if (r.ok) { window.location.href = r.redirect || '/my/articles'; }
|
||||
else { blogShowError('myArticleError', r.error || 'Failed to save article.'); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
||||
|
||||
@@ -40,9 +40,10 @@
|
||||
<td class="px-4 py-3 text-sm text-right whitespace-nowrap">
|
||||
<a href="/my/articles/{{.ID}}/edit"
|
||||
class="text-blue-600 hover:text-blue-800 font-medium mr-3">{{index $.Tr "article_edit"}}</a>
|
||||
<form action="/my/articles/{{.ID}}/delete" method="post" class="inline"
|
||||
onsubmit="return confirm('{{index $.Tr "article_delete_confirm"}}');">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<form class="inline blog-delete-form"
|
||||
data-url="/api/my/articles/{{.ID}}"
|
||||
data-confirm="{{index $.Tr "article_delete_confirm"}}"
|
||||
onsubmit="return blogDelete(this)">
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "article_delete"}}</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user