作品展示页:前端展示 + 后台增删改查

- 新增 projects 表与迁移(唯一 slug 索引)
- 后端:公开 GET /api/projects,后台 GET/POST/PUT/DELETE /api/admin/projects
- 前端:/projects 双虚线边框卡片页(参考 diygod.cc/projects),后台管理页
- 左栏/手机顶栏/后台侧栏均加「作品」入口
- 补充项目 CRUD 单元测试与后台鉴权拦截测试
This commit is contained in:
Sakurasan
2026-09-22 01:47:59 +08:00
parent 2b53eb976b
commit 3238175ef0
14 changed files with 1029 additions and 1 deletions
+88
View File
@@ -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) {