作品展示页:前端展示 + 后台增删改查
- 新增 projects 表与迁移(唯一 slug 索引) - 后端:公开 GET /api/projects,后台 GET/POST/PUT/DELETE /api/admin/projects - 前端:/projects 双虚线边框卡片页(参考 diygod.cc/projects),后台管理页 - 左栏/手机顶栏/后台侧栏均加「作品」入口 - 补充项目 CRUD 单元测试与后台鉴权拦截测试
This commit is contained in:
@@ -54,6 +54,8 @@ func (a *API) Routes() http.Handler {
|
||||
mux.HandleFunc("/api/admin/posts/", a.guard(a.postByID))
|
||||
mux.HandleFunc("/api/admin/tags", a.guard(a.listTags))
|
||||
mux.HandleFunc("/api/admin/tags/", a.guard(a.tagByID))
|
||||
mux.HandleFunc("/api/admin/projects", a.guard(a.listProjects))
|
||||
mux.HandleFunc("/api/admin/projects/", a.guard(a.projectByID))
|
||||
mux.HandleFunc("/api/admin/settings", a.guard(a.settings))
|
||||
return mux
|
||||
}
|
||||
@@ -355,6 +357,92 @@ func writeTag(w http.ResponseWriter, t model.Tag, err error) {
|
||||
httpx.OK(w, t)
|
||||
}
|
||||
|
||||
// ---------- projects ----------
|
||||
|
||||
func (a *API) listProjects(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
status := httpx.QueryString(r, "status")
|
||||
projects, err := a.Store.ListProjects(status)
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
if projects == nil {
|
||||
projects = []model.Project{}
|
||||
}
|
||||
httpx.OK(w, map[string]any{"projects": projects})
|
||||
case http.MethodPost:
|
||||
var in model.ProjectInput
|
||||
if err := httpx.Decode(r, &in); err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Title) == "" {
|
||||
httpx.BadRequest(w, "title required")
|
||||
return
|
||||
}
|
||||
p, err := a.Store.CreateProject(in)
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.Created(w, p)
|
||||
default:
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "GET/POST required")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) projectByID(w http.ResponseWriter, r *http.Request) {
|
||||
rest := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/admin/projects/"), "/")
|
||||
if rest == "" {
|
||||
a.listProjects(w, r)
|
||||
return
|
||||
}
|
||||
id, err := parseInt(rest)
|
||||
if err != nil {
|
||||
httpx.BadRequest(w, "bad project id")
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
p, err := a.Store.GetProject(id)
|
||||
writeProject(w, p, err)
|
||||
case http.MethodPut, http.MethodPatch:
|
||||
var in model.ProjectInput
|
||||
if err := httpx.Decode(r, &in); err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
p, err := a.Store.UpdateProject(id, in)
|
||||
writeProject(w, p, err)
|
||||
case http.MethodDelete:
|
||||
if err := a.Store.DeleteProject(id); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, map[string]any{"ok": true})
|
||||
default:
|
||||
httpx.Error(w, http.StatusMethodNotAllowed, "GET/PUT/DELETE required")
|
||||
}
|
||||
}
|
||||
|
||||
func writeProject(w http.ResponseWriter, p model.Project, err error) {
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.OK(w, p)
|
||||
}
|
||||
|
||||
// ---------- settings ----------
|
||||
|
||||
func (a *API) settings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user