feat: Admin 设用户等级并展示徽章于用户页
补齐 SetUserLevel 表单;用户主页/资料页展示徽章并触发 EvaluateAuto;勾选 §H Exp 与防刷。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -129,11 +129,11 @@
|
|||||||
- [x] 每日签到:基础 5,连签每日 +1,封顶 15 — `POST /profile/checkin`
|
- [x] 每日签到:基础 5,连签每日 +1,封顶 15 — `POST /profile/checkin`
|
||||||
- [x] 每日抽奖:奖池加权(0/2/5/10/20),成本 0 — `POST /profile/lottery`
|
- [x] 每日抽奖:奖池加权(0/2/5/10/20),成本 0 — `POST /profile/lottery`
|
||||||
- [x] 积分解锁分成:读者付全额,作者约 70% — 门控 unlock 服务
|
- [x] 积分解锁分成:读者付全额,作者约 70% — 门控 unlock 服务
|
||||||
- [ ] 短龄同 IP 互刷拒绝分成
|
- [x] 短龄同 IP 互刷拒绝分成 — `UnlockPointsBlock` / `suspiciousUnlockPair`(双方注册未满 7 天且 LastLoginIP 相同)
|
||||||
- [ ] Exp → 等级 Lv1–10
|
- [x] Exp → 等级 Lv1–10 — 门槛推导 + 发帖/评论/获赞加 Exp;Admin 设等级 SSR `/admin/users`
|
||||||
- [ ] 自动徽章(注册天数 / 获赞 / 创作分成)— 种子定义 + `EvaluateAuto` 已有;Admin 可改定义,独立调度 UI 未做
|
- [x] 自动徽章(注册天数 / 获赞 / 创作分成)— 种子 + `EvaluateAuto`(用户页/资料页访问时评估);无独立定时调度 UI
|
||||||
- [x] 限定徽章:管理员定义与授予/撤销 — SSR `/admin/badges`
|
- [x] 限定徽章:管理员定义与授予/撤销 — SSR `/admin/badges`
|
||||||
- [x] 管理员调整积分、设定认证(免审)— SSR `/admin/users`(等级设定 UI 未做,仅只读展示 Lv)
|
- [x] 管理员调整积分、设定认证(免审)、设定等级 — SSR `/admin/users`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
- 公开页:签名、等级/积分只读、统计、最近帖 — SSR `/user/:id`(无邮箱)
|
- 公开页:签名、等级/积分只读、统计、最近帖 — SSR `/user/:id`(无邮箱)
|
||||||
- 收藏列表 — SSR `/favorites`
|
- 收藏列表 — SSR `/favorites`
|
||||||
- 积分钱包面板(余额、近 N 条流水、签到/抽奖表单 PRG)— SSR `/profile`;导航展示当前积分
|
- 积分钱包面板(余额、近 N 条流水、签到/抽奖表单 PRG)— SSR `/profile`;导航展示当前积分
|
||||||
- 徽章展示:**未迁**
|
- 徽章展示:用户主页 / 个人中心 — SSR(访问时 `EvaluateAuto`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
| Posts | 审核通过/拒绝 — SSR `/admin/moderation`;置顶/版顶/精华/锁编/锁评/软删 — 帖详情 Admin 条;回收站恢复/清除 — SSR `/admin/trash` |
|
| Posts | 审核通过/拒绝 — SSR `/admin/moderation`;置顶/版顶/精华/锁编/锁评/软删 — 帖详情 Admin 条;回收站恢复/清除 — SSR `/admin/trash` |
|
||||||
| Comments | 审核 — SSR `/admin/moderation`;修订查看未迁 |
|
| Comments | 审核 — SSR `/admin/moderation`;修订查看未迁 |
|
||||||
| Reports | 处理动作:dismiss / resolve / reject_post / reject_comment — SSR `/admin/reports` |
|
| Reports | 处理动作:dismiss / resolve / reject_post / reject_comment — SSR `/admin/reports` |
|
||||||
| Users | 搜索;禁言;认证;调积分 — SSR `/admin/users`(设等级 / 授徽章挂在徽章页) |
|
| Users | 搜索;禁言;认证;调积分;设等级 — SSR `/admin/users`(授徽章在 `/admin/badges`) |
|
||||||
| Badges | 定义自动/限定徽章;颁发/收回限定 — SSR `/admin/badges` |
|
| Badges | 定义自动/限定徽章;颁发/收回限定 — SSR `/admin/badges` |
|
||||||
| Media | 分类浏览;单删/批量删;同步索引 — SSR `/admin/media` |
|
| Media | 分类浏览;单删/批量删;同步索引 — SSR `/admin/media` |
|
||||||
| Media | 分类浏览;批量删 |
|
| Media | 分类浏览;批量删 |
|
||||||
|
|||||||
@@ -12,15 +12,15 @@
|
|||||||
|
|
||||||
| 项 | 值 |
|
| 项 | 值 |
|
||||||
|----|-----|
|
|----|-----|
|
||||||
| **上一刀** | Admin `/admin/media`(列表 / 删除 / 同步索引) |
|
| **上一刀** | P2:Admin 设等级 + 用户页徽章;勾选 Exp / 防刷 / 自动徽章 |
|
||||||
| **下一刀** | **等级/自动徽章 UI** 或 **防刷分成**(`02` §H P2;开刀前确认偏好) |
|
| **下一刀** | **P3 体验**(主题 / 伪静态 / 编辑器)或 **后置**(OIDC / 存储热切换 / Gitea);需点名 |
|
||||||
| **工作区** | 应干净;有未提交改动时先处理再开新刀 |
|
| **工作区** | 应干净;有未提交改动时先处理再开新刀 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一句话状态
|
## 一句话状态
|
||||||
|
|
||||||
论坛核心闭环 + Admin 用户/徽章/媒体 **已迁完**。余量主要是成长体系与防刷、OIDC/存储产品化与体验打磨。
|
核心论坛 + Admin P1 + §H 成长/防刷(服务层 + Admin 设等级)**已齐**。余量:P3 体验打磨与后置产品化(OIDC / S3 热切换 / Gitea)。
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -28,43 +28,28 @@ flowchart LR
|
|||||||
admin[Admin用户徽章媒体]
|
admin[Admin用户徽章媒体]
|
||||||
growth[等级徽章防刷]
|
growth[等级徽章防刷]
|
||||||
polish[主题伪静态编辑器]
|
polish[主题伪静态编辑器]
|
||||||
deferred[Gitea码桶后置]
|
deferred[OIDC存储Gitea]
|
||||||
core --> admin --> growth
|
core --> admin --> growth --> polish
|
||||||
admin --> polish
|
deferred -.-> polish
|
||||||
deferred -.-> admin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已完成里程碑
|
## 已完成里程碑
|
||||||
|
|
||||||
### 公开站
|
### 公开站 / Admin / §H
|
||||||
|
|
||||||
| 域 | 状态 |
|
| 域 | 状态 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| `/install`、登录/注册/忘记密码、opaque session | 已迁 |
|
| 五种帖类型、评论、私信、友链、单页、§O | 已迁 |
|
||||||
| Feed / 板块 / 搜索 / 三栏 / 右栏 widgets | 已迁 |
|
| Admin:users / badges / media / settings 基础 | 已迁 |
|
||||||
| 帖详情:赞藏举报、门控、审核横幅、修订历史 | 已迁 |
|
| Exp→Lv、Admin 设等级、用户页徽章展示 | 已迁 |
|
||||||
| 帖类型:`normal` / `question` / `poll` / `bounty` / `lottery` | 已迁 |
|
| 短龄同 IP 互刷拒绝分成 | 已落地(unlock 服务) |
|
||||||
| 评论:发评/回复/赞/私密/编辑删除/@/回复通知 | 已迁(扁平楼层;嵌套树未做) |
|
| 自动徽章 EvaluateAuto | 已落地(访问用户页触发) |
|
||||||
| 私信/系统通知、收藏、个人中心、用户页 | 已迁 |
|
|
||||||
| 友链前台 + Admin(含复检) | 已迁 |
|
|
||||||
| 站点单页 | 已迁 |
|
|
||||||
| `/health`、`robots.txt`、`sitemap.xml`、`/uploads` | 已落地(`02` §O 已勾) |
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
|
|
||||||
| 路径 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| dashboard / boards / moderation / reports / trash | 已迁 |
|
|
||||||
| settings:品牌、限流、敏感词、SMTP、侧栏、SQLite 备份 | 已迁 |
|
|
||||||
| friend-links / pages | 已迁 |
|
|
||||||
| **users / badges / media** | **已迁** |
|
|
||||||
| settings:OIDC / Gitea / 存储 / 伪静态 Tab | **未迁** |
|
|
||||||
|
|
||||||
### 近期提交(摘)
|
### 近期提交(摘)
|
||||||
|
|
||||||
Admin users → badges → **media**
|
media → **等级设定 + 徽章展示 / §H 勾选**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -72,31 +57,25 @@ Admin users → badges → **media**
|
|||||||
|
|
||||||
### P0 — 小清理(可夹带)
|
### P0 — 小清理(可夹带)
|
||||||
|
|
||||||
- 发评路径挂 `RateLimiter` `comment` 动作(服务层已有键,web 未挂)
|
- 发评路径挂 `RateLimiter` `comment` 动作
|
||||||
- Logo/Favicon/OG 上传、头像裁剪等声明为未做的边角
|
- Logo/Favicon/OG 上传、头像裁剪
|
||||||
|
|
||||||
### P1 — Admin 核心
|
### P2 — 成长与防刷
|
||||||
|
|
||||||
1. ~~Admin 用户~~ ✓
|
~~已完成主路径~~;无独立「自动徽章定时任务」Admin UI(可接受)。
|
||||||
2. ~~Admin 徽章~~ ✓
|
|
||||||
3. ~~Admin 媒体~~ ✓
|
|
||||||
|
|
||||||
### P2 — 成长与防刷 ← 当前指针候选
|
### P3 — 体验 / 编辑器 ← 当前候选
|
||||||
|
|
||||||
- Exp → Lv1–10 设定 UI、自动徽章调度说明、短龄同 IP 互刷拒绝分成(`02` §H)
|
|
||||||
|
|
||||||
### P3 — 体验 / 编辑器(可砍或长期)
|
|
||||||
|
|
||||||
- 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin
|
- 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin
|
||||||
- TipTap / Markdown 双模(现行为 textarea + 门控)
|
- TipTap / Markdown 双模
|
||||||
- 游客评论、评论嵌套树(建议:全局 `#floor` + 缩进)
|
- 游客评论、评论嵌套树
|
||||||
- 修订 diff、完整 Limits 字数
|
- 修订 diff、完整 Limits 字数
|
||||||
|
|
||||||
### 后置(不阻塞验收)
|
### 后置
|
||||||
|
|
||||||
- K:Gitea `/projects`
|
- K:Gitea `/projects`
|
||||||
- L:OIDC 产品化 Admin CRUD
|
- L:OIDC 产品化 Admin CRUD
|
||||||
- M:S3 热切换、WebP thumb 全链路产品化
|
- M:S3 热切换、WebP 全链路产品化
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -104,39 +83,15 @@ Admin users → badges → **media**
|
|||||||
|
|
||||||
| 序 | 刀 | 产出 |
|
| 序 | 刀 | 产出 |
|
||||||
|----|----|------|
|
|----|----|------|
|
||||||
| ✓ | 五种帖类型 + 备份/复检 + §O | 核心闭环 |
|
| ✓ | 核心闭环 + Admin P1 | … |
|
||||||
| ✓ | Admin users / badges / media | Admin P1 |
|
| ✓ | §H 等级/防刷/徽章展示 | … |
|
||||||
| → | 等级设定 **或** 防刷分成 | 需偏好 |
|
| → | 需点名 P3 或后置 | … |
|
||||||
| … | P3 / 后置 | 需明确点名再开 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 下一刀实现提纲:P2 候选
|
## 已完成计划
|
||||||
|
|
||||||
> 「继续」前请点名:**等级/Exp UI**、**防刷分成**,或其它(OIDC/伪静态等)。
|
- [admin-users](plans/admin-users.md) · [admin-badges](plans/admin-badges.md) · [admin-media](plans/admin-media.md)
|
||||||
|
|
||||||
### A. 等级
|
|
||||||
|
|
||||||
- Admin 设用户 Exp/Lv(`SetUserLevel` 已有)
|
|
||||||
- 公开页等级展示对齐
|
|
||||||
|
|
||||||
### B. 防刷分成
|
|
||||||
|
|
||||||
- 短龄同 IP 互刷拒绝创作分成(`02` §H)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 已完成:Admin 媒体
|
|
||||||
|
|
||||||
见 [plans/admin-media.md](plans/admin-media.md)。
|
|
||||||
|
|
||||||
## 已完成:Admin 徽章
|
|
||||||
|
|
||||||
见 [plans/admin-badges.md](plans/admin-badges.md)。
|
|
||||||
|
|
||||||
## 已完成:Admin 用户管理
|
|
||||||
|
|
||||||
见 [plans/admin-users.md](plans/admin-users.md)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
17
docs/rebuild-spec/plans/level-badges-display.md
Normal file
17
docs/rebuild-spec/plans/level-badges-display.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# P2 等级设定与徽章展示 — 实现计划
|
||||||
|
|
||||||
|
> 状态:已完成
|
||||||
|
> 分支:`rebuild/gitea-ssr`
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
| 能力 | 实现 |
|
||||||
|
|------|------|
|
||||||
|
| Admin 设等级 | `POST /admin/users/:id/level` → `SetUserLevel`(Exp 调至门槛) |
|
||||||
|
| 公开等级 | 既有 `/user/:id`、`/profile` Lv/Exp |
|
||||||
|
| 徽章展示 | 用户页 / 资料页列表;访问时 `EvaluateAuto` |
|
||||||
|
| 防刷分成 | 确认 `suspiciousUnlockPair` 已落地并勾选 `02` |
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
`build.bat`;冒烟:设用户 Lv5 → Exp=200;用户页可见等级。
|
||||||
@@ -1040,6 +1040,33 @@ points-only[data-locked="false"] {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.j13-admin-level {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.j13-admin-level select {
|
||||||
|
min-width: 4.5rem;
|
||||||
|
}
|
||||||
|
.j13-user-badges {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.j13-user-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: 1px solid var(--j13-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--j13-muted);
|
||||||
|
}
|
||||||
|
.j13-user-badge[title] { cursor: help; }
|
||||||
.j13-admin-badge-award-btns {
|
.j13-admin-badge-award-btns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -30,15 +30,16 @@ type adminUserRow struct {
|
|||||||
|
|
||||||
type adminUsersData struct {
|
type adminUsersData struct {
|
||||||
AdminChrome
|
AdminChrome
|
||||||
Users []adminUserRow
|
Users []adminUserRow
|
||||||
Keyword string
|
Keyword string
|
||||||
Filter string
|
Filter string
|
||||||
Page int
|
Page int
|
||||||
HasPrev bool
|
HasPrev bool
|
||||||
HasMore bool
|
HasMore bool
|
||||||
PrevPage int
|
PrevPage int
|
||||||
NextPage int
|
NextPage int
|
||||||
QuerySuffix string
|
QuerySuffix string
|
||||||
|
LevelOptions []int
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminUsersGet 用户列表
|
// AdminUsersGet 用户列表
|
||||||
@@ -71,6 +72,7 @@ func (d Deps) renderAdminUsers(ctx *webctx.Context, errMsg string) {
|
|||||||
PrevPage: page - 1,
|
PrevPage: page - 1,
|
||||||
NextPage: page + 1,
|
NextPage: page + 1,
|
||||||
HasPrev: page > 1,
|
HasPrev: page > 1,
|
||||||
|
LevelOptions: adminLevelOptions(),
|
||||||
}
|
}
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
@@ -120,6 +122,15 @@ func adminUserRoleLabel(role models.Role) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adminLevelOptions() []int {
|
||||||
|
n := models.MaxLevel()
|
||||||
|
out := make([]int, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out[i] = i + 1
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// AdminUserBanPost 禁言 / 解禁
|
// AdminUserBanPost 禁言 / 解禁
|
||||||
func (d Deps) AdminUserBanPost(c *gin.Context) {
|
func (d Deps) AdminUserBanPost(c *gin.Context) {
|
||||||
ctx := d.ctx(c)
|
ctx := d.ctx(c)
|
||||||
@@ -192,3 +203,24 @@ func (d Deps) AdminUserPointsPost(c *gin.Context) {
|
|||||||
ctx.SetFlash(fmt.Sprintf("积分已调整,当前余额 %d", bal))
|
ctx.SetFlash(fmt.Sprintf("积分已调整,当前余额 %d", bal))
|
||||||
ctx.Redirect("/admin/users")
|
ctx.Redirect("/admin/users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminUserLevelPost 设定等级(同步 Exp 到门槛)
|
||||||
|
func (d Deps) AdminUserLevelPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminUsers(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
level, err := strconv.Atoi(strings.TrimSpace(c.PostForm("level")))
|
||||||
|
if err != nil {
|
||||||
|
d.renderAdminUsers(ctx, "请选择有效等级")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := services.SetUserLevel(uint(id64), level); err != nil {
|
||||||
|
d.renderAdminUsers(ctx, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash(fmt.Sprintf("已设为 Lv%d(经验门槛 %d)", level, models.ExpForLevel(level)))
|
||||||
|
ctx.Redirect("/admin/users")
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
admin.POST("/users/:id/ban", deps.AdminUserBanPost)
|
admin.POST("/users/:id/ban", deps.AdminUserBanPost)
|
||||||
admin.POST("/users/:id/verify", deps.AdminUserVerifyPost)
|
admin.POST("/users/:id/verify", deps.AdminUserVerifyPost)
|
||||||
admin.POST("/users/:id/points", deps.AdminUserPointsPost)
|
admin.POST("/users/:id/points", deps.AdminUserPointsPost)
|
||||||
|
admin.POST("/users/:id/level", deps.AdminUserLevelPost)
|
||||||
admin.GET("/badges", deps.AdminBadgesGet)
|
admin.GET("/badges", deps.AdminBadgesGet)
|
||||||
admin.POST("/badges", deps.AdminBadgeCreate)
|
admin.POST("/badges", deps.AdminBadgeCreate)
|
||||||
admin.POST("/badges/award", deps.AdminBadgeAwardPost)
|
admin.POST("/badges/award", deps.AdminBadgeAwardPost)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type profileData struct {
|
|||||||
Level int
|
Level int
|
||||||
Exp int
|
Exp int
|
||||||
Points int
|
Points int
|
||||||
|
Badges []userPublicBadge
|
||||||
PostCount int64
|
PostCount int64
|
||||||
CommentCount int64
|
CommentCount int64
|
||||||
FavoriteCount int64
|
FavoriteCount int64
|
||||||
@@ -112,6 +113,14 @@ func (d Deps) renderProfile(ctx *webctx.Context, errMsg string) {
|
|||||||
AvatarMaxMB: d.Settings.AvatarMaxMB(),
|
AvatarMaxMB: d.Settings.AvatarMaxMB(),
|
||||||
SignatureMax: d.Settings.SignatureMax(),
|
SignatureMax: d.Settings.SignatureMax(),
|
||||||
}
|
}
|
||||||
|
if d.Badge != nil {
|
||||||
|
_ = d.Badge.EvaluateAuto(uid)
|
||||||
|
if rows, err := d.Badge.ListUserBadges(uid); err == nil {
|
||||||
|
for _, v := range services.BadgeViews(rows, 0) {
|
||||||
|
data.Badges = append(data.Badges, userPublicBadge{Name: v.Name, Description: v.Description})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if d.Points != nil {
|
if d.Points != nil {
|
||||||
data.CheckIn, _ = d.Points.GetCheckInStatus(uid)
|
data.CheckIn, _ = d.Points.GetCheckInStatus(uid)
|
||||||
data.Lottery, _ = d.Points.GetLotteryStatus(uid)
|
data.Lottery, _ = d.Points.GetLotteryStatus(uid)
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type userPublicBadge struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
type userPublicData struct {
|
type userPublicData struct {
|
||||||
PageChrome
|
PageChrome
|
||||||
UserID uint
|
UserID uint
|
||||||
@@ -20,6 +25,7 @@ type userPublicData struct {
|
|||||||
Level int
|
Level int
|
||||||
Exp int
|
Exp int
|
||||||
Points int
|
Points int
|
||||||
|
Badges []userPublicBadge
|
||||||
PostCount int64
|
PostCount int64
|
||||||
CommentCount int64
|
CommentCount int64
|
||||||
FavoriteCount int64
|
FavoriteCount int64
|
||||||
@@ -75,6 +81,15 @@ func (d Deps) UserPublic(c *gin.Context) {
|
|||||||
nick := displayName(user)
|
nick := displayName(user)
|
||||||
chrome := d.chrome(ctx, nick+" · "+d.Settings.SiteBranding().Name, strings.TrimSpace(user.Signature), "")
|
chrome := d.chrome(ctx, nick+" · "+d.Settings.SiteBranding().Name, strings.TrimSpace(user.Signature), "")
|
||||||
hasMore := int64(page*size) < total
|
hasMore := int64(page*size) < total
|
||||||
|
badges := []userPublicBadge{}
|
||||||
|
if d.Badge != nil {
|
||||||
|
_ = d.Badge.EvaluateAuto(user.ID)
|
||||||
|
if rows, err := d.Badge.ListUserBadges(user.ID); err == nil {
|
||||||
|
for _, v := range services.BadgeViews(rows, 0) {
|
||||||
|
badges = append(badges, userPublicBadge{Name: v.Name, Description: v.Description})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.HTML(http.StatusOK, "user/view", userPublicData{
|
ctx.HTML(http.StatusOK, "user/view", userPublicData{
|
||||||
PageChrome: chrome,
|
PageChrome: chrome,
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
@@ -85,6 +100,7 @@ func (d Deps) UserPublic(c *gin.Context) {
|
|||||||
Level: models.LevelFromExp(user.Exp),
|
Level: models.LevelFromExp(user.Exp),
|
||||||
Exp: user.Exp,
|
Exp: user.Exp,
|
||||||
Points: user.Points,
|
Points: user.Points,
|
||||||
|
Badges: badges,
|
||||||
PostCount: st.PostCount, CommentCount: st.CommentCount,
|
PostCount: st.PostCount, CommentCount: st.CommentCount,
|
||||||
FavoriteCount: st.FavoriteCount, LikeReceived: st.LikeReceived,
|
FavoriteCount: st.FavoriteCount, LikeReceived: st.LikeReceived,
|
||||||
IsSelf: ctx.UserID() == user.ID,
|
IsSelf: ctx.UserID() == user.ID,
|
||||||
|
|||||||
@@ -86,6 +86,16 @@
|
|||||||
<input name="note" maxlength="120" placeholder="备注(可选)" style="width:8rem"/>
|
<input name="note" maxlength="120" placeholder="备注(可选)" style="width:8rem"/>
|
||||||
<button type="submit" class="j13-btn-secondary">调积分</button>
|
<button type="submit" class="j13-btn-secondary">调积分</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" action="/admin/users/{{.ID}}/level" class="j13-form j13-admin-level">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
|
||||||
|
{{$cur := .Level}}
|
||||||
|
<select name="level" aria-label="设定等级">
|
||||||
|
{{range $.LevelOptions}}
|
||||||
|
<option value="{{.}}"{{if eq . $cur}} selected{{end}}>Lv{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="j13-btn-secondary">设等级</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -75,6 +75,13 @@
|
|||||||
<p>用户名 <strong>{{.Username}}</strong></p>
|
<p>用户名 <strong>{{.Username}}</strong></p>
|
||||||
<p class="j13-muted">邮箱 {{.Email}}</p>
|
<p class="j13-muted">邮箱 {{.Email}}</p>
|
||||||
<p class="j13-muted">等级 Lv.{{.Level}} · 经验 {{.Exp}}</p>
|
<p class="j13-muted">等级 Lv.{{.Level}} · 经验 {{.Exp}}</p>
|
||||||
|
{{if .Badges}}
|
||||||
|
<ul class="j13-user-badges">
|
||||||
|
{{range .Badges}}
|
||||||
|
<li class="j13-user-badge"{{if .Description}} title="{{.Description}}"{{end}}>{{.Name}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
{{if .Avatar}}
|
{{if .Avatar}}
|
||||||
<p><img class="j13-avatar" src="{{.Avatar}}" alt="头像"/></p>
|
<p><img class="j13-avatar" src="{{.Avatar}}" alt="头像"/></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>{{if .Nickname}}{{.Nickname}}{{else}}{{.Username}}{{end}}</h1>
|
<h1>{{if .Nickname}}{{.Nickname}}{{else}}{{.Username}}{{end}}</h1>
|
||||||
<p class="j13-muted">@{{.Username}} · Lv.{{.Level}} · 积分 {{.Points}}</p>
|
<p class="j13-muted">@{{.Username}} · Lv.{{.Level}} · 积分 {{.Points}}</p>
|
||||||
|
{{if .Badges}}
|
||||||
|
<ul class="j13-user-badges">
|
||||||
|
{{range .Badges}}
|
||||||
|
<li class="j13-user-badge"{{if .Description}} title="{{.Description}}"{{end}}>{{.Name}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
{{if .Signature}}<p class="j13-user-sig">{{.Signature}}</p>{{end}}
|
{{if .Signature}}<p class="j13-user-sig">{{.Signature}}</p>{{end}}
|
||||||
{{if .IsSelf}}<p><a href="/profile">编辑资料</a></p>
|
{{if .IsSelf}}<p><a href="/profile">编辑资料</a></p>
|
||||||
{{else if .LoggedIn}}<p><a href="/messages/with/{{.UserID}}">发私信</a></p>{{end}}
|
{{else if .LoggedIn}}<p><a href="/messages/with/{{.UserID}}">发私信</a></p>{{end}}
|
||||||
|
|||||||
@@ -1040,6 +1040,33 @@ points-only[data-locked="false"] {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.j13-admin-level {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.j13-admin-level select {
|
||||||
|
min-width: 4.5rem;
|
||||||
|
}
|
||||||
|
.j13-user-badges {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.j13-user-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: 1px solid var(--j13-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--j13-muted);
|
||||||
|
}
|
||||||
|
.j13-user-badge[title] { cursor: help; }
|
||||||
.j13-admin-badge-award-btns {
|
.j13-admin-badge-award-btns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user