feat: Admin 用户列表禁言、认证与调积分
SSR /admin/users 复用 ListUsers/BanUser/SetVerified/AdminAdjust,并回写 02/06/09 指针至徽章。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -133,7 +133,7 @@
|
|||||||
- [ ] Exp → 等级 Lv1–10
|
- [ ] Exp → 等级 Lv1–10
|
||||||
- [ ] 自动徽章(注册天数 / 获赞 / 创作分成)
|
- [ ] 自动徽章(注册天数 / 获赞 / 创作分成)
|
||||||
- [ ] 限定徽章:管理员定义与授予/撤销
|
- [ ] 限定徽章:管理员定义与授予/撤销
|
||||||
- [ ] 管理员调整积分、设定等级
|
- [x] 管理员调整积分、设定认证(免审)— SSR `/admin/users`(等级设定 UI 未做,仅只读展示 Lv)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,11 @@
|
|||||||
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
||||||
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
||||||
| `/admin/reports` | 举报处理 | 已迁 |
|
| `/admin/reports` | 举报处理 | 已迁 |
|
||||||
|
| `/admin/users` | 用户列表、禁言、认证、调积分 | 已迁 |
|
||||||
| `/admin/trash` | 帖回收站 | 已迁 |
|
| `/admin/trash` | 帖回收站 | 已迁 |
|
||||||
| `/admin/login` | 重定向前台登录 | 已迁 |
|
| `/admin/login` | 重定向前台登录 | 已迁 |
|
||||||
|
|
||||||
未迁(原 SPA):**users(下一刀,见 [09](09-ssr-progress.md))** / badges / media / 完整 Limits 等。
|
未迁(原 SPA):badges / media / 完整 Limits 等。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
| 项 | 值 |
|
| 项 | 值 |
|
||||||
|----|-----|
|
|----|-----|
|
||||||
| **上一刀** | SSR 抽奖帖 `lottery`(已推送 `e06c548`) |
|
| **上一刀** | Admin `/admin/users`(禁言 / 认证 / 调积分) |
|
||||||
| **下一刀** | **Admin `/admin/users`**(禁言 / 验证 / 调积分)— 见下文「下一刀实现提纲」 |
|
| **下一刀** | **Admin `/admin/badges`**(徽章定义 CRUD + 授予/撤销) |
|
||||||
| **工作区** | 应干净;有未提交改动时先处理再开新刀 |
|
| **工作区** | 应干净;有未提交改动时先处理再开新刀 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -59,12 +59,13 @@ flowchart LR
|
|||||||
| dashboard / boards / moderation / reports / trash | 已迁 |
|
| dashboard / boards / moderation / reports / trash | 已迁 |
|
||||||
| settings:品牌、限流、敏感词、SMTP、侧栏、SQLite 备份 | 已迁 |
|
| settings:品牌、限流、敏感词、SMTP、侧栏、SQLite 备份 | 已迁 |
|
||||||
| friend-links / pages | 已迁 |
|
| friend-links / pages | 已迁 |
|
||||||
| **users / badges / media** | **未迁** |
|
| **users**(禁言 / 认证 / 调积分) | **已迁** |
|
||||||
|
| **badges / media** | **未迁** |
|
||||||
| settings:OIDC / Gitea / 存储 / 伪静态 Tab | **未迁** |
|
| settings:OIDC / Gitea / 存储 / 伪静态 Tab | **未迁** |
|
||||||
|
|
||||||
### 近期提交(摘)
|
### 近期提交(摘)
|
||||||
|
|
||||||
`poll` → SQLite 备份 → 友链复检 → `question` → `bounty` → `lottery`(`e06c548`)
|
`poll` → SQLite 备份 → 友链复检 → `question` → `bounty` → `lottery` → **Admin users**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,8 +78,8 @@ flowchart LR
|
|||||||
|
|
||||||
### P1 — 推荐刀序(服务层多半已齐)
|
### P1 — 推荐刀序(服务层多半已齐)
|
||||||
|
|
||||||
1. **Admin 用户** ← 当前指针
|
1. ~~Admin 用户~~ ✓
|
||||||
2. Admin 徽章定义 + 授予/撤销
|
2. **Admin 徽章** ← 当前指针
|
||||||
3. Admin 媒体列表/删除
|
3. Admin 媒体列表/删除
|
||||||
|
|
||||||
### P2 — 成长与防刷
|
### P2 — 成长与防刷
|
||||||
@@ -89,7 +90,7 @@ flowchart LR
|
|||||||
|
|
||||||
- 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin
|
- 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin
|
||||||
- TipTap / Markdown 双模(现行为 textarea + 门控)
|
- TipTap / Markdown 双模(现行为 textarea + 门控)
|
||||||
- 游客评论、评论嵌套树(建议:全局 `#floor` + 缩进,编号语义先写入 `06`)
|
- 游客评论、评论嵌套树(建议:全局 `#floor` + 缩进)
|
||||||
- 修订 diff、完整 Limits 字数
|
- 修订 diff、完整 Limits 字数
|
||||||
|
|
||||||
### 后置(不阻塞验收)
|
### 后置(不阻塞验收)
|
||||||
@@ -106,36 +107,33 @@ flowchart LR
|
|||||||
|----|----|------|
|
|----|----|------|
|
||||||
| ✓ | 五种帖类型 + 备份/复检 + §O 勾选 | 核心闭环 |
|
| ✓ | 五种帖类型 + 备份/复检 + §O 勾选 | 核心闭环 |
|
||||||
| ✓ | 本文 `09` | 可查阅进度 |
|
| ✓ | 本文 `09` | 可查阅进度 |
|
||||||
| → | Admin `/admin/users` | 禁言/验证/调积分 |
|
| ✓ | Admin `/admin/users` | 禁言/认证/调积分 |
|
||||||
| 3 | Admin `/admin/badges` | 徽章 CRUD + 授予 |
|
| → | Admin `/admin/badges` | 徽章 CRUD + 授予 |
|
||||||
| 4 | Admin `/admin/media` | 媒体列表删除 |
|
| 4 | Admin `/admin/media` | 媒体列表删除 |
|
||||||
| 5 | 等级/自动徽章 **或** 防刷分成 | 按当时偏好 |
|
| 5 | 等级/自动徽章 **或** 防刷分成 | 按当时偏好 |
|
||||||
| … | P3 / 后置 | 需明确点名再开 |
|
| … | P3 / 后置 | 需明确点名再开 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 下一刀实现提纲:Admin 用户管理
|
## 下一刀实现提纲:Admin 徽章
|
||||||
|
|
||||||
> 确认「继续」后按此实现;细则副本:[plans/admin-users.md](plans/admin-users.md)。不恢复论坛管理 JSON `/api` 为 UI 主路径。
|
> 确认「继续」后实现限定徽章定义与授予/撤销;复用 `services/badge.go`。
|
||||||
|
|
||||||
### 范围
|
### 范围(草案)
|
||||||
|
|
||||||
- 路由:`GET /admin/users`;`POST /admin/users/:id/ban`、`/verify`、`/points`
|
- `GET/POST /admin/badges`:徽章定义列表与创建/编辑/删除
|
||||||
- 复用:`UserService.ListUsers` / `BanUser`;`SetVerified`;`PointsService.AdminAdjust`
|
- 授予/撤销:挂在用户页或徽章详情(`GrantBadge` / 撤销)
|
||||||
- 模板:`templates/admin/users.tmpl` + `admin/nav` 入口
|
- 回写 `02` §H 限定徽章;指针 → media
|
||||||
- 列表:分页、关键词;展示角色、积分、禁言、verified
|
|
||||||
- 操作:禁言/解禁、切换认证、调积分(delta + 备注);CSRF + PRG
|
|
||||||
- 规格:勾选 `02` §H「管理员调整积分、设定等级」中与积分/认证相关部分(等级 UI 可后置);`06` 标明 `/admin/users` 已迁
|
|
||||||
|
|
||||||
### 不做(本刀)
|
### 不做
|
||||||
|
|
||||||
- 设定 Exp/等级 UI(可只读展示 level)
|
自动徽章规则引擎(P2)、等级设定 UI。
|
||||||
- 徽章授予(下一刀 badges)
|
|
||||||
- 恢复 `/api/admin/users` 给浏览器
|
|
||||||
|
|
||||||
### 验证
|
---
|
||||||
|
|
||||||
- `build.bat`;冒烟:列表 → 禁言 → 调积分 → 认证开关
|
## 已完成:Admin 用户管理
|
||||||
|
|
||||||
|
见 [plans/admin-users.md](plans/admin-users.md);路由 `/admin/users` + ban / verify / points。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Admin 用户管理(SSR)— 实现计划
|
# Admin 用户管理(SSR)— 实现计划
|
||||||
|
|
||||||
> 状态:待执行(指针见 [09-ssr-progress.md](../docs/rebuild-spec/09-ssr-progress.md))
|
> 状态:已完成(SSR `/admin/users`)
|
||||||
> 分支:`rebuild/gitea-ssr`
|
> 分支:`rebuild/gitea-ssr`
|
||||||
|
|
||||||
## 范围
|
## 范围
|
||||||
|
|||||||
@@ -1026,6 +1026,20 @@ points-only[data-locked="false"] {
|
|||||||
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
||||||
.j13-admin-filters a.is-active,
|
.j13-admin-filters a.is-active,
|
||||||
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
||||||
|
.j13-admin-user-ops {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
.j13-admin-points {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.j13-post__admin {
|
.j13-post__admin {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
194
routers/web/admin_users.go
Normal file
194
routers/web/admin_users.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/models"
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adminUserRow struct {
|
||||||
|
ID uint
|
||||||
|
Username string
|
||||||
|
Nickname string
|
||||||
|
Email string
|
||||||
|
Role string
|
||||||
|
RoleLabel string
|
||||||
|
Points int
|
||||||
|
Level int
|
||||||
|
Verified bool
|
||||||
|
Banned bool
|
||||||
|
IsAdmin bool
|
||||||
|
Created string
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminUsersData struct {
|
||||||
|
AdminChrome
|
||||||
|
Users []adminUserRow
|
||||||
|
Keyword string
|
||||||
|
Filter string
|
||||||
|
Page int
|
||||||
|
HasPrev bool
|
||||||
|
HasMore bool
|
||||||
|
PrevPage int
|
||||||
|
NextPage int
|
||||||
|
QuerySuffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUsersGet 用户列表
|
||||||
|
func (d Deps) AdminUsersGet(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
d.renderAdminUsers(ctx, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) renderAdminUsers(ctx *webctx.Context, errMsg string) {
|
||||||
|
chrome := d.adminChrome(ctx, "用户", "users")
|
||||||
|
chrome.Error = errMsg
|
||||||
|
page, _ := strconv.Atoi(ctx.C.DefaultQuery("page", "1"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
keyword := strings.TrimSpace(ctx.C.Query("q"))
|
||||||
|
filter := strings.TrimSpace(ctx.C.DefaultQuery("filter", "all"))
|
||||||
|
if filter == "" {
|
||||||
|
filter = "all"
|
||||||
|
}
|
||||||
|
size := d.Settings.PageSizeDefault()
|
||||||
|
if size < 1 {
|
||||||
|
size = 20
|
||||||
|
}
|
||||||
|
data := adminUsersData{
|
||||||
|
AdminChrome: chrome,
|
||||||
|
Keyword: keyword,
|
||||||
|
Filter: filter,
|
||||||
|
Page: page,
|
||||||
|
PrevPage: page - 1,
|
||||||
|
NextPage: page + 1,
|
||||||
|
HasPrev: page > 1,
|
||||||
|
}
|
||||||
|
q := url.Values{}
|
||||||
|
if keyword != "" {
|
||||||
|
q.Set("q", keyword)
|
||||||
|
}
|
||||||
|
if filter != "" && filter != "all" {
|
||||||
|
q.Set("filter", filter)
|
||||||
|
}
|
||||||
|
data.QuerySuffix = q.Encode()
|
||||||
|
|
||||||
|
if d.User != nil {
|
||||||
|
list, total, err := d.User.ListUsers(services.UserListQuery{
|
||||||
|
Page: page, Size: size, Keyword: keyword, Filter: filter,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
chrome.Error = err.Error()
|
||||||
|
data.AdminChrome = chrome
|
||||||
|
ctx.HTML(http.StatusOK, "admin/users", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.HasMore = int64(page*size) < total
|
||||||
|
data.Users = make([]adminUserRow, 0, len(list))
|
||||||
|
for _, u := range list {
|
||||||
|
nick := strings.TrimSpace(u.Nickname)
|
||||||
|
if nick == "" {
|
||||||
|
nick = u.Username
|
||||||
|
}
|
||||||
|
data.Users = append(data.Users, adminUserRow{
|
||||||
|
ID: u.ID, Username: u.Username, Nickname: nick, Email: u.Email,
|
||||||
|
Role: string(u.Role), RoleLabel: adminUserRoleLabel(u.Role),
|
||||||
|
Points: u.Points, Level: models.LevelFromExp(u.Exp),
|
||||||
|
Verified: u.Verified, Banned: u.Banned,
|
||||||
|
IsAdmin: u.Role == models.RoleAdmin,
|
||||||
|
Created: u.CreatedAt.Format("2006-01-02"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.HTML(http.StatusOK, "admin/users", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminUserRoleLabel(role models.Role) string {
|
||||||
|
switch role {
|
||||||
|
case models.RoleAdmin:
|
||||||
|
return "管理员"
|
||||||
|
default:
|
||||||
|
return "用户"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserBanPost 禁言 / 解禁
|
||||||
|
func (d Deps) AdminUserBanPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminUsers(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.User == nil {
|
||||||
|
d.renderAdminUsers(ctx, "用户服务未就绪")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
banned := c.PostForm("banned") == "1" || c.PostForm("banned") == "on"
|
||||||
|
if err := d.User.BanUser(uint(id64), banned); err != nil {
|
||||||
|
d.renderAdminUsers(ctx, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if banned {
|
||||||
|
ctx.SetFlash("已禁言该用户")
|
||||||
|
} else {
|
||||||
|
ctx.SetFlash("已解除禁言")
|
||||||
|
}
|
||||||
|
ctx.Redirect("/admin/users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserVerifyPost 认证开关
|
||||||
|
func (d Deps) AdminUserVerifyPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminUsers(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
verified := c.PostForm("verified") == "1" || c.PostForm("verified") == "on"
|
||||||
|
if err := services.SetVerified(uint(id64), verified); err != nil {
|
||||||
|
d.renderAdminUsers(ctx, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if verified {
|
||||||
|
ctx.SetFlash("已设为认证用户")
|
||||||
|
} else {
|
||||||
|
ctx.SetFlash("已取消认证")
|
||||||
|
}
|
||||||
|
ctx.Redirect("/admin/users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserPointsPost 调整积分
|
||||||
|
func (d Deps) AdminUserPointsPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminUsers(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.Points == nil {
|
||||||
|
d.renderAdminUsers(ctx, "积分服务未就绪")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
delta, err := strconv.Atoi(strings.TrimSpace(c.PostForm("delta")))
|
||||||
|
if err != nil || delta == 0 {
|
||||||
|
d.renderAdminUsers(ctx, "请填写非零整数增减额")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
note := strings.TrimSpace(c.PostForm("note"))
|
||||||
|
bal, err := d.Points.AdminAdjust(uint(id64), delta, note)
|
||||||
|
if err != nil {
|
||||||
|
d.renderAdminUsers(ctx, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash(fmt.Sprintf("积分已调整,当前余额 %d", bal))
|
||||||
|
ctx.Redirect("/admin/users")
|
||||||
|
}
|
||||||
@@ -97,6 +97,10 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
|
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
|
||||||
admin.GET("/reports", deps.AdminReportsGet)
|
admin.GET("/reports", deps.AdminReportsGet)
|
||||||
admin.POST("/reports/:id/handle", deps.AdminReportHandlePost)
|
admin.POST("/reports/:id/handle", deps.AdminReportHandlePost)
|
||||||
|
admin.GET("/users", deps.AdminUsersGet)
|
||||||
|
admin.POST("/users/:id/ban", deps.AdminUserBanPost)
|
||||||
|
admin.POST("/users/:id/verify", deps.AdminUserVerifyPost)
|
||||||
|
admin.POST("/users/:id/points", deps.AdminUserPointsPost)
|
||||||
admin.GET("/trash", deps.AdminTrashGet)
|
admin.GET("/trash", deps.AdminTrashGet)
|
||||||
admin.POST("/trash/:id/restore", deps.AdminTrashRestorePost)
|
admin.POST("/trash/:id/restore", deps.AdminTrashRestorePost)
|
||||||
admin.POST("/trash/:id/purge", deps.AdminTrashPurgePost)
|
admin.POST("/trash/:id/purge", deps.AdminTrashPurgePost)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<a href="/admin/boards"{{if eq .NavActive "boards"}} class="is-active"{{end}}>板块</a>
|
<a href="/admin/boards"{{if eq .NavActive "boards"}} class="is-active"{{end}}>板块</a>
|
||||||
<a href="/admin/moderation"{{if eq .NavActive "moderation"}} class="is-active"{{end}}>审核</a>
|
<a href="/admin/moderation"{{if eq .NavActive "moderation"}} class="is-active"{{end}}>审核</a>
|
||||||
<a href="/admin/reports"{{if eq .NavActive "reports"}} class="is-active"{{end}}>举报</a>
|
<a href="/admin/reports"{{if eq .NavActive "reports"}} class="is-active"{{end}}>举报</a>
|
||||||
|
<a href="/admin/users"{{if eq .NavActive "users"}} class="is-active"{{end}}>用户</a>
|
||||||
<a href="/admin/trash"{{if eq .NavActive "trash"}} class="is-active"{{end}}>回收站</a>
|
<a href="/admin/trash"{{if eq .NavActive "trash"}} class="is-active"{{end}}>回收站</a>
|
||||||
<a href="/admin/friend-links"{{if eq .NavActive "friend-links"}} class="is-active"{{end}}>友链</a>
|
<a href="/admin/friend-links"{{if eq .NavActive "friend-links"}} class="is-active"{{end}}>友链</a>
|
||||||
<a href="/admin/pages"{{if eq .NavActive "pages"}} class="is-active"{{end}}>单页</a>
|
<a href="/admin/pages"{{if eq .NavActive "pages"}} class="is-active"{{end}}>单页</a>
|
||||||
|
|||||||
110
templates/admin/users.tmpl
Normal file
110
templates/admin/users.tmpl
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{{define "admin/users"}}
|
||||||
|
{{template "base/head" .}}
|
||||||
|
{{template "base/navbar" .}}
|
||||||
|
<main class="j13-main j13-admin">
|
||||||
|
<h1>用户管理</h1>
|
||||||
|
{{template "admin/nav" .}}
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
|
||||||
|
<form method="get" action="/admin/users" class="j13-form j13-admin-form j13-admin-search">
|
||||||
|
<label>搜索 <input name="q" value="{{.Keyword}}" placeholder="用户名 / 昵称 / 邮箱 / ID"/></label>
|
||||||
|
<label>筛选
|
||||||
|
<select name="filter">
|
||||||
|
<option value="all"{{if eq .Filter "all"}} selected{{end}}>全部</option>
|
||||||
|
<option value="verified"{{if eq .Filter "verified"}} selected{{end}}>认证用户</option>
|
||||||
|
<option value="banned"{{if eq .Filter "banned"}} selected{{end}}>已禁言</option>
|
||||||
|
<option value="admin"{{if eq .Filter "admin"}} selected{{end}}>管理员</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="submit">查询</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="j13-admin-filters">
|
||||||
|
<a href="/admin/users"{{if eq .Filter "all"}} class="is-active"{{end}}>全部</a>
|
||||||
|
<a href="/admin/users?filter=verified"{{if eq .Filter "verified"}} class="is-active"{{end}}>认证</a>
|
||||||
|
<a href="/admin/users?filter=banned"{{if eq .Filter "banned"}} class="is-active"{{end}}>禁言</a>
|
||||||
|
<a href="/admin/users?filter=admin"{{if eq .Filter "admin"}} class="is-active"{{end}}>管理员</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{if .Users}}
|
||||||
|
<table class="j13-admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>角色</th>
|
||||||
|
<th>积分 / Lv</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>注册</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Users}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ID}}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/user/{{.ID}}">{{.Nickname}}</a>
|
||||||
|
<div class="j13-muted">@{{.Username}}{{if .Email}} · {{.Email}}{{end}}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{.RoleLabel}}</td>
|
||||||
|
<td>{{.Points}} · Lv{{.Level}}</td>
|
||||||
|
<td>
|
||||||
|
{{if .Banned}}<span class="j13-tag j13-tag--rejected">禁言</span>{{end}}
|
||||||
|
{{if .Verified}}<span class="j13-tag j13-tag--resolved">认证</span>{{end}}
|
||||||
|
{{if and (not .Banned) (not .Verified)}}<span class="j13-muted">—</span>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>{{.Created}}</td>
|
||||||
|
<td class="j13-admin-user-ops">
|
||||||
|
{{if not .IsAdmin}}
|
||||||
|
<form method="post" action="/admin/users/{{.ID}}/ban" class="j13-inline-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
{{if .Banned}}
|
||||||
|
<input type="hidden" name="banned" value="0"/>
|
||||||
|
<button type="submit" class="j13-btn-secondary">解禁</button>
|
||||||
|
{{else}}
|
||||||
|
<input type="hidden" name="banned" value="1"/>
|
||||||
|
<button type="submit">禁言</button>
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/users/{{.ID}}/verify" class="j13-inline-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
{{if .Verified}}
|
||||||
|
<input type="hidden" name="verified" value="0"/>
|
||||||
|
<button type="submit" class="j13-btn-secondary">取消认证</button>
|
||||||
|
{{else}}
|
||||||
|
<input type="hidden" name="verified" value="1"/>
|
||||||
|
<button type="submit" class="j13-btn-secondary">设为认证</button>
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<span class="j13-muted">管理员不可禁言</span>
|
||||||
|
{{end}}
|
||||||
|
<form method="post" action="/admin/users/{{.ID}}/points" class="j13-form j13-admin-points">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
<input name="delta" type="number" required placeholder="±积分" style="width:5.5rem"/>
|
||||||
|
<input name="note" maxlength="120" placeholder="备注(可选)" style="width:8rem"/>
|
||||||
|
<button type="submit" class="j13-btn-secondary">调积分</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{if or .HasPrev .HasMore}}
|
||||||
|
<nav class="j13-pager">
|
||||||
|
{{if .HasPrev}}
|
||||||
|
<a href="/admin/users?{{if .QuerySuffix}}{{.QuerySuffix}}&{{end}}page={{.PrevPage}}">上一页</a>
|
||||||
|
{{end}}
|
||||||
|
<span>第 {{.Page}} 页</span>
|
||||||
|
{{if .HasMore}}
|
||||||
|
<a href="/admin/users?{{if .QuerySuffix}}{{.QuerySuffix}}&{{end}}page={{.NextPage}}">下一页</a>
|
||||||
|
{{end}}
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<p class="j13-empty">没有匹配的用户。</p>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{template "base/footer" .}}
|
||||||
|
{{end}}
|
||||||
@@ -1026,6 +1026,20 @@ points-only[data-locked="false"] {
|
|||||||
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
.j13-admin-filters a { color: var(--j13-muted); text-decoration: none; }
|
||||||
.j13-admin-filters a.is-active,
|
.j13-admin-filters a.is-active,
|
||||||
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
.j13-admin-filters a:hover { color: var(--j13-accent); font-weight: 600; }
|
||||||
|
.j13-admin-user-ops {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
.j13-admin-points {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.j13-post__admin {
|
.j13-post__admin {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user