feat: 完整内容 Limits 与伪静态 Admin
设置页可配字数/编辑窗/分页与伪静态后缀;公开帖/板/用户链接走规范路径并 301。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
- [x] 右栏:热门帖、标签云、最新评论、最新用户、友链(可开关排序)— SSR 热门固定 + `aside_widgets`;Admin `/admin/settings` 侧栏组件编辑器
|
- [x] 右栏:热门帖、标签云、最新评论、最新用户、友链(可开关排序)— SSR 热门固定 + `aside_widgets`;Admin `/admin/settings` 侧栏组件编辑器
|
||||||
- [x] 登录用户右栏/侧边:签到与抽奖入口 — SSR 右栏条(钱包详情仍在 `/profile#wallet`)
|
- [x] 登录用户右栏/侧边:签到与抽奖入口 — SSR 右栏条(钱包详情仍在 `/profile#wallet`)
|
||||||
- [ ] 下拉刷新(移动端)
|
- [ ] 下拉刷新(移动端)
|
||||||
- [ ] 可选伪静态:`/post/123.html` 等形式(后缀后台可配)
|
- [x] 可选伪静态:`/post/123.html` 等形式(后缀后台可配)— SSR `/admin/settings` 伪静态;公开路径 301 规范 URL
|
||||||
- [x] 404 页
|
- [x] 404 页
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -187,7 +187,8 @@
|
|||||||
|
|
||||||
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 + 待处理举报 — SSR `/admin/dashboard`
|
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 + 待处理举报 — SSR `/admin/dashboard`
|
||||||
- [x] 敏感词:`forum_settings.filter_words` 读写 + 热更 — SSR `/admin/settings`
|
- [x] 敏感词:`forum_settings.filter_words` 读写 + 热更 — SSR `/admin/settings`
|
||||||
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
- [x] 基础限流(post/comment/register/login/window)— SSR `/admin/settings`
|
||||||
|
- [x] 完整 Limits(字数 / 编辑窗 / 分页 / 签名头像 / Feed 样式 / 新标签)— SSR `/admin/settings/content-limits`
|
||||||
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
||||||
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR `/admin/settings`
|
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR `/admin/settings`
|
||||||
- [x] Logo / Favicon / 默认 OG 图上传与清除 — SSR `/admin/settings/brand/upload|clear`;`<head>` favicon + og:image
|
- [x] Logo / Favicon / 默认 OG 图上传与清除 — SSR `/admin/settings/brand/upload|clear`;`<head>` favicon + og:image
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
| `/admin/dashboard` | 概览计数 | 已迁 |
|
| `/admin/dashboard` | 概览计数 | 已迁 |
|
||||||
| `/admin/boards` | 板块 CRUD | 已迁 |
|
| `/admin/boards` | 板块 CRUD | 已迁 |
|
||||||
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
||||||
| `/admin/settings` | 品牌(含 Logo/Favicon/OG 上传)+ 基础限流 + 敏感词 + SMTP + 备份 | 已迁 |
|
| `/admin/settings` | 品牌(含 Logo/Favicon/OG)+ 限流 + 内容限制 + 伪静态 + 敏感词 + SMTP + 备份 | 已迁 |
|
||||||
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
||||||
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
| `/admin/pages` | 站点单页 CRUD / 发布 | 已迁 |
|
||||||
| `/admin/reports` | 举报处理 | 已迁 |
|
| `/admin/reports` | 举报处理 | 已迁 |
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
| `/admin/trash` | 帖回收站 | 已迁 |
|
| `/admin/trash` | 帖回收站 | 已迁 |
|
||||||
| `/admin/login` | 重定向前台登录 | 已迁 |
|
| `/admin/login` | 重定向前台登录 | 已迁 |
|
||||||
|
|
||||||
未迁(原 SPA):完整 Limits / OIDC·Gitea·存储热切换 Admin 等。
|
未迁(原 SPA):OIDC·Gitea·存储热切换 Admin 等。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
| Badges | 定义自动/限定徽章;颁发/收回限定 — SSR `/admin/badges` |
|
| Badges | 定义自动/限定徽章;颁发/收回限定 — SSR `/admin/badges` |
|
||||||
| Media | 分类浏览;单删/批量删;同步索引 — SSR `/admin/media` |
|
| Media | 分类浏览;单删/批量删;同步索引 — SSR `/admin/media` |
|
||||||
| Media | 分类浏览;批量删 |
|
| Media | 分类浏览;批量删 |
|
||||||
| Settings | Tab:论坛限制、侧栏组件、伪静态、邮件、OIDC+客户端、Gitea、存储、品牌、敏感词、备份 — SSR `/admin/settings` 已迁品牌(含 Logo/Favicon/OG)/限流/侧栏/敏感词/邮件/SQLite 备份(OIDC/Gitea/存储/伪静态未迁) |
|
| Settings | 品牌(含 Logo/Favicon/OG)、限流、内容限制、伪静态、侧栏、敏感词、邮件、SQLite 备份 — SSR `/admin/settings`(OIDC/Gitea/存储热切换未迁) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
> **分支**:`rebuild/gitea-ssr`(`main` = SPA 对照)
|
> **分支**:`rebuild/gitea-ssr`(`main` = SPA 对照)
|
||||||
> **验收清单**: [02-features.md](02-features.md)
|
> **验收清单**: [02-features.md](02-features.md)
|
||||||
> **架构**: [08-gitea-ssr-architecture.md](08-gitea-ssr-architecture.md)
|
> **架构**: [08-gitea-ssr-architecture.md](08-gitea-ssr-architecture.md)
|
||||||
> **维护约定**:每次「继续」开新刀前更新本文「当前指针」;功能勾选以 `02` 为准,本文管阶段与刀序。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,15 +11,15 @@
|
|||||||
|
|
||||||
| 项 | 值 |
|
| 项 | 值 |
|
||||||
|----|-----|
|
|----|-----|
|
||||||
| **上一刀** | P0:发评限流 + 品牌 Logo/Favicon/OG 上传与 head meta |
|
| **上一刀** | 完整 Limits + 伪静态 Admin / 规范 URL |
|
||||||
| **下一刀** | **P3 / 后置**(需点名:伪静态、完整 Limits、OIDC、S3 热切换、编辑器、嵌套评论等) |
|
| **下一刀** | **后置或体验**(需点名:OIDC、S3 热切换、编辑器、嵌套评论、Gitea…) |
|
||||||
| **工作区** | 应干净 |
|
| **工作区** | 应干净 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一句话状态
|
## 一句话状态
|
||||||
|
|
||||||
核心论坛、Admin P1、§H 成长/防刷、P0 品牌图与发评限流 **已齐**。余量主要是体验打磨与后置产品化。
|
核心论坛、Admin、§H、品牌图、发评限流、**完整 Limits 与伪静态**已齐。余量多为后置产品化与体验打磨。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,37 +27,22 @@
|
|||||||
|
|
||||||
| 域 | 状态 |
|
| 域 | 状态 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| 五种帖 + Admin users/badges/media | 已迁 |
|
| Admin users / badges / media | 已迁 |
|
||||||
| §H Exp/等级/徽章/防刷 | 已迁 |
|
| §H 等级 / 徽章 / 防刷 | 已迁 |
|
||||||
| 发评 `RateLimiter` comment | 已挂 |
|
| 品牌 Logo/Favicon/OG + 发评限流 | 已迁 |
|
||||||
| 品牌 Logo/Favicon/OG 上传 + favicon/og meta | 已迁 |
|
| 内容 Limits + 伪静态 | 已迁 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 未完成分层
|
## 未完成
|
||||||
|
|
||||||
### P0 余量
|
### P3 体验
|
||||||
|
|
||||||
- 头像裁剪等边角
|
- 主题、侧栏折叠、虚拟滚动、TipTap、游客评论、嵌套树、修订 diff
|
||||||
|
|
||||||
### P3 — 体验 / 编辑器
|
|
||||||
|
|
||||||
- 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin
|
|
||||||
- TipTap / Markdown 双模
|
|
||||||
- 游客评论、评论嵌套树
|
|
||||||
- 修订 diff、完整 Limits 字数
|
|
||||||
|
|
||||||
### 后置
|
### 后置
|
||||||
|
|
||||||
- K:Gitea `/projects`
|
- OIDC Admin CRUD、S3 热切换 UI、Gitea `/projects`、WebP 产品化
|
||||||
- L:OIDC 产品化 Admin CRUD
|
|
||||||
- M:S3 热切换、WebP 全链路产品化
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 默认「继续」
|
|
||||||
|
|
||||||
需点名下一刀;未点名时优先小清理或明确产品偏好。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -68,4 +52,3 @@
|
|||||||
|------|--------|
|
|------|--------|
|
||||||
| 某功能做没做? | [02-features.md](02-features.md) |
|
| 某功能做没做? | [02-features.md](02-features.md) |
|
||||||
| 现在该干什么? | 本文「当前指针」 |
|
| 现在该干什么? | 本文「当前指针」 |
|
||||||
| 路由迁没迁? | [06-pages-ux.md](06-pages-ux.md) §1 |
|
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ type adminSettingsData struct {
|
|||||||
RateReg int
|
RateReg int
|
||||||
RateLogin int
|
RateLogin int
|
||||||
RateWindow int
|
RateWindow int
|
||||||
|
Limits services.ForumLimits
|
||||||
FilterWords string
|
FilterWords string
|
||||||
FilterCount int
|
FilterCount int
|
||||||
Mail services.MailConfig
|
Mail services.MailConfig
|
||||||
@@ -465,6 +466,7 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
|||||||
RateReg: lim.RateLimitRegister,
|
RateReg: lim.RateLimitRegister,
|
||||||
RateLogin: lim.RateLimitLogin,
|
RateLogin: lim.RateLimitLogin,
|
||||||
RateWindow: lim.RateLimitWindowSec,
|
RateWindow: lim.RateLimitWindowSec,
|
||||||
|
Limits: lim,
|
||||||
FilterWords: words,
|
FilterWords: words,
|
||||||
FilterCount: services.CountFilterWords(words),
|
FilterCount: services.CountFilterWords(words),
|
||||||
Mail: mail,
|
Mail: mail,
|
||||||
@@ -624,6 +626,51 @@ func (d Deps) AdminSettingsLimitsPost(c *gin.Context) {
|
|||||||
ctx.Redirect("/admin/settings")
|
ctx.Redirect("/admin/settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminSettingsContentLimitsPost 字数与编辑窗等
|
||||||
|
func (d Deps) AdminSettingsContentLimitsPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atoi := func(name string) int {
|
||||||
|
n, _ := strconv.Atoi(strings.TrimSpace(c.PostForm(name)))
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
err := d.Settings.UpdateContentLimits(
|
||||||
|
atoi("post_edit_window_hours"), atoi("comment_edit_window_minutes"),
|
||||||
|
atoi("post_title_max"), atoi("post_tags_max"), atoi("post_content_max"), atoi("comment_max"),
|
||||||
|
atoi("search_keyword_min"), atoi("search_keyword_max"), atoi("page_size_default"),
|
||||||
|
atoi("password_min_len"), atoi("avatar_max_mb"), atoi("signature_max"),
|
||||||
|
c.PostForm("open_posts_in_new_tab") == "1" || c.PostForm("open_posts_in_new_tab") == "on",
|
||||||
|
c.PostForm("open_content_links_in_new_tab") == "1" || c.PostForm("open_content_links_in_new_tab") == "on",
|
||||||
|
strings.TrimSpace(c.PostForm("feed_list_style")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.renderAdminSettings(ctx, "内容限制无效,请检查数值范围")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash("内容限制已保存")
|
||||||
|
ctx.Redirect("/admin/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminSettingsPermalinkPost 伪静态
|
||||||
|
func (d Deps) AdminSettingsPermalinkPost(c *gin.Context) {
|
||||||
|
ctx := d.ctx(c)
|
||||||
|
if !ctx.CheckCSRF() {
|
||||||
|
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enabled := c.PostForm("permalink_enabled") == "1" || c.PostForm("permalink_enabled") == "on"
|
||||||
|
ext := strings.TrimSpace(c.PostForm("permalink_ext"))
|
||||||
|
if err := d.Settings.UpdatePermalinkSettings(enabled, ext); err != nil {
|
||||||
|
d.renderAdminSettings(ctx, "伪静态后缀无效(1–16 位字母数字)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetFlash("伪静态设置已保存")
|
||||||
|
ctx.Redirect("/admin/settings")
|
||||||
|
}
|
||||||
|
|
||||||
// AdminSettingsFilterWordsPost 敏感词
|
// AdminSettingsFilterWordsPost 敏感词
|
||||||
func (d Deps) AdminSettingsFilterWordsPost(c *gin.Context) {
|
func (d Deps) AdminSettingsFilterWordsPost(c *gin.Context) {
|
||||||
ctx := d.ctx(c)
|
ctx := d.ctx(c)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type boardsItemView struct {
|
type boardsItemView struct {
|
||||||
ID uint
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
|
Href string
|
||||||
Description string
|
Description string
|
||||||
Icon string
|
Icon string
|
||||||
ColorIndex int
|
ColorIndex int
|
||||||
@@ -32,6 +33,7 @@ func (d Deps) renderBoards(ctx *webctx.Context) {
|
|||||||
chrome := d.chrome(ctx, "板块 · "+brand.Name, "", "")
|
chrome := d.chrome(ctx, "板块 · "+brand.Name, "", "")
|
||||||
list, _ := d.Board.ListWithStats()
|
list, _ := d.Board.ListWithStats()
|
||||||
items := make([]boardsItemView, 0, len(list))
|
items := make([]boardsItemView, 0, len(list))
|
||||||
|
pl := d.permalink()
|
||||||
for _, b := range list {
|
for _, b := range list {
|
||||||
color := b.ColorIndex
|
color := b.ColorIndex
|
||||||
if color < 0 {
|
if color < 0 {
|
||||||
@@ -40,6 +42,7 @@ func (d Deps) renderBoards(ctx *webctx.Context) {
|
|||||||
items = append(items, boardsItemView{
|
items = append(items, boardsItemView{
|
||||||
ID: b.ID,
|
ID: b.ID,
|
||||||
Name: b.Name,
|
Name: b.Name,
|
||||||
|
Href: pl.BoardPath(b.ID),
|
||||||
Description: b.Description,
|
Description: b.Description,
|
||||||
Icon: b.Icon,
|
Icon: b.Icon,
|
||||||
ColorIndex: color,
|
ColorIndex: color,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||||
@@ -44,12 +45,14 @@ type SitePageLink struct {
|
|||||||
type BoardView struct {
|
type BoardView struct {
|
||||||
ID uint
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
|
Href string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RightAsideHotPost 右栏热门帖
|
// RightAsideHotPost 右栏热门帖
|
||||||
type RightAsideHotPost struct {
|
type RightAsideHotPost struct {
|
||||||
ID uint
|
ID uint
|
||||||
Title string
|
Title string
|
||||||
|
Href string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RightAsideTag 右栏标签
|
// RightAsideTag 右栏标签
|
||||||
@@ -61,6 +64,7 @@ type RightAsideTag struct {
|
|||||||
// RightAsideComment 右栏最新评论
|
// RightAsideComment 右栏最新评论
|
||||||
type RightAsideComment struct {
|
type RightAsideComment struct {
|
||||||
PostID uint
|
PostID uint
|
||||||
|
PostHref string
|
||||||
Author string
|
Author string
|
||||||
Excerpt string
|
Excerpt string
|
||||||
PostTitle string
|
PostTitle string
|
||||||
@@ -70,6 +74,7 @@ type RightAsideComment struct {
|
|||||||
type RightAsideUser struct {
|
type RightAsideUser struct {
|
||||||
ID uint
|
ID uint
|
||||||
Nickname string
|
Nickname string
|
||||||
|
Href string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RightAsideFriendLink 右栏友链
|
// RightAsideFriendLink 右栏友链
|
||||||
@@ -111,6 +116,8 @@ type PageChrome struct {
|
|||||||
LogoURL string
|
LogoURL string
|
||||||
FaviconURL string
|
FaviconURL string
|
||||||
OGImageURL string
|
OGImageURL string
|
||||||
|
PermalinkSuffix string // 如 ".html";未启用为空
|
||||||
|
OpenPostsInNewTab bool
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
ViewerName string
|
ViewerName string
|
||||||
@@ -151,8 +158,9 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
|||||||
}
|
}
|
||||||
boards, _ := d.Board.List()
|
boards, _ := d.Board.List()
|
||||||
bv := make([]BoardView, 0, len(boards))
|
bv := make([]BoardView, 0, len(boards))
|
||||||
|
pl := d.permalink()
|
||||||
for _, b := range boards {
|
for _, b := range boards {
|
||||||
bv = append(bv, BoardView{ID: b.ID, Name: b.Name})
|
bv = append(bv, BoardView{ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID)})
|
||||||
}
|
}
|
||||||
var unread int64
|
var unread int64
|
||||||
if ctx.IsSigned() && d.Message != nil {
|
if ctx.IsSigned() && d.Message != nil {
|
||||||
@@ -176,6 +184,8 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
|||||||
LogoURL: strings.TrimSpace(brand.Logo),
|
LogoURL: strings.TrimSpace(brand.Logo),
|
||||||
FaviconURL: strings.TrimSpace(brand.Favicon),
|
FaviconURL: strings.TrimSpace(brand.Favicon),
|
||||||
OGImageURL: strings.TrimSpace(brand.DefaultShareImage()),
|
OGImageURL: strings.TrimSpace(brand.DefaultShareImage()),
|
||||||
|
PermalinkSuffix: d.Settings.Permalink().Suffix(),
|
||||||
|
OpenPostsInNewTab: d.Settings.OpenPostsInNewTab(),
|
||||||
LoggedIn: ctx.IsSigned(),
|
LoggedIn: ctx.IsSigned(),
|
||||||
IsAdmin: ctx.IsAdmin(),
|
IsAdmin: ctx.IsAdmin(),
|
||||||
ViewerName: name,
|
ViewerName: name,
|
||||||
@@ -242,8 +252,9 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
|||||||
}
|
}
|
||||||
if d.Post != nil {
|
if d.Post != nil {
|
||||||
if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil {
|
if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil {
|
||||||
|
pl := d.permalink()
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title})
|
out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,9 +275,10 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
|||||||
case services.AsideWidgetRecentComments:
|
case services.AsideWidgetRecentComments:
|
||||||
if d.Comment != nil {
|
if d.Comment != nil {
|
||||||
if list, err := d.Comment.ListRecentPublic(rightAsideCommentLimit); err == nil {
|
if list, err := d.Comment.ListRecentPublic(rightAsideCommentLimit); err == nil {
|
||||||
|
pl := d.permalink()
|
||||||
for _, c := range list {
|
for _, c := range list {
|
||||||
block.Comments = append(block.Comments, RightAsideComment{
|
block.Comments = append(block.Comments, RightAsideComment{
|
||||||
PostID: c.PostID, Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle,
|
PostID: c.PostID, PostHref: pl.PostPath(c.PostID), Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,8 +286,9 @@ func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) R
|
|||||||
case services.AsideWidgetRecentUsers:
|
case services.AsideWidgetRecentUsers:
|
||||||
if d.User != nil {
|
if d.User != nil {
|
||||||
if list, err := d.User.ListRecentRegistered(rightAsideUserLimit); err == nil {
|
if list, err := d.User.ListRecentRegistered(rightAsideUserLimit); err == nil {
|
||||||
|
pl := d.permalink()
|
||||||
for _, u := range list {
|
for _, u := range list {
|
||||||
block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname})
|
block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname, Href: pl.UserPath(u.ID)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,3 +324,39 @@ func stripIDParam(raw, permalinkExt string) string {
|
|||||||
raw = strings.TrimSuffix(raw, ".htm")
|
raw = strings.TrimSuffix(raw, ".htm")
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d Deps) permalink() services.PermalinkConfig {
|
||||||
|
if d.Settings == nil {
|
||||||
|
return services.PermalinkConfig{}
|
||||||
|
}
|
||||||
|
return d.Settings.Permalink()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) postHref(id uint) string {
|
||||||
|
return d.permalink().PostPath(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) boardHref(id uint) string {
|
||||||
|
return d.permalink().BoardPath(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) userHref(id uint) string {
|
||||||
|
return d.permalink().UserPath(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirectIfNotCanonical 伪静态开启时把无后缀/错误后缀 301 到规范路径
|
||||||
|
func (d Deps) redirectIfNotCanonical(c *gin.Context, match services.PermalinkMatch) bool {
|
||||||
|
if !match.OK || !d.permalink().Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if !match.NeedsCanonicalRedirect(path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
loc := match.Canonical
|
||||||
|
if q := c.Request.URL.RawQuery; q != "" {
|
||||||
|
loc = loc + "?" + q
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusMovedPermanently, loc)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (d Deps) ComposePost(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
ctx.SetFlash("发帖成功")
|
ctx.SetFlash("发帖成功")
|
||||||
}
|
}
|
||||||
ctx.Redirect(fmt.Sprintf("/post/%d", post.ID))
|
ctx.Redirect(d.postHref(post.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostEditGet 编辑帖
|
// PostEditGet 编辑帖
|
||||||
@@ -152,7 +152,7 @@ func (d Deps) PostEditPost(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.SetFlash("已保存")
|
ctx.SetFlash("已保存")
|
||||||
ctx.Redirect(fmt.Sprintf("/post/%d", post.ID))
|
ctx.Redirect(d.postHref(post.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeUpload 帖图上传(JSON,供 compose 页 fetch)
|
// ComposeUpload 帖图上传(JSON,供 compose 页 fetch)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type favItem struct {
|
type favItem struct {
|
||||||
PostID uint
|
PostID uint
|
||||||
Title string
|
Title string
|
||||||
|
Href string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
BoardName string
|
BoardName string
|
||||||
CreatedLabel string
|
CreatedLabel string
|
||||||
@@ -43,6 +44,7 @@ func (d Deps) FavoritesGet(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]favItem, 0, len(favs))
|
items := make([]favItem, 0, len(favs))
|
||||||
|
pl := d.permalink()
|
||||||
for _, f := range favs {
|
for _, f := range favs {
|
||||||
title := f.Post.Title
|
title := f.Post.Title
|
||||||
author := strings.TrimSpace(f.Post.User.Nickname)
|
author := strings.TrimSpace(f.Post.User.Nickname)
|
||||||
@@ -54,7 +56,7 @@ func (d Deps) FavoritesGet(c *gin.Context) {
|
|||||||
board = f.Post.Board.Name
|
board = f.Post.Board.Name
|
||||||
}
|
}
|
||||||
items = append(items, favItem{
|
items = append(items, favItem{
|
||||||
PostID: f.PostID, Title: title, AuthorName: author, BoardName: board,
|
PostID: f.PostID, Title: title, Href: pl.PostPath(f.PostID), AuthorName: author, BoardName: board,
|
||||||
CreatedLabel: formatTime(f.CreatedAt),
|
CreatedLabel: formatTime(f.CreatedAt),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
admin.POST("/settings/brand/upload", deps.AdminSettingsBrandUploadPost)
|
admin.POST("/settings/brand/upload", deps.AdminSettingsBrandUploadPost)
|
||||||
admin.POST("/settings/brand/clear", deps.AdminSettingsBrandClearPost)
|
admin.POST("/settings/brand/clear", deps.AdminSettingsBrandClearPost)
|
||||||
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
||||||
|
admin.POST("/settings/content-limits", deps.AdminSettingsContentLimitsPost)
|
||||||
|
admin.POST("/settings/permalink", deps.AdminSettingsPermalinkPost)
|
||||||
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
|
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
|
||||||
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
||||||
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
||||||
@@ -165,6 +167,7 @@ type HomePageData struct {
|
|||||||
type PostListItem struct {
|
type PostListItem struct {
|
||||||
ID uint
|
ID uint
|
||||||
Title string
|
Title string
|
||||||
|
Href string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
BoardName string
|
BoardName string
|
||||||
Pinned bool
|
Pinned bool
|
||||||
@@ -176,22 +179,22 @@ type PostListItem struct {
|
|||||||
// FormAction 搜索表单提交路径(当前 Feed)
|
// FormAction 搜索表单提交路径(当前 Feed)
|
||||||
func (d HomePageData) FormAction() string {
|
func (d HomePageData) FormAction() string {
|
||||||
if d.ActiveBoard > 0 {
|
if d.ActiveBoard > 0 {
|
||||||
return fmt.Sprintf("/board/%d", d.ActiveBoard)
|
return fmt.Sprintf("/board/%d%s", d.ActiveBoard, d.PermalinkSuffix)
|
||||||
}
|
}
|
||||||
return "/"
|
return "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortHref 排序链接,保留搜索参数
|
// SortHref 排序链接,保留搜索参数
|
||||||
func (d HomePageData) SortHref(sort string) string {
|
func (d HomePageData) SortHref(sort string) string {
|
||||||
return buildFeedURL(d.ActiveBoard, sort, 0, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
return buildFeedURL(d.ActiveBoard, d.PermalinkSuffix, sort, 0, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageHref 分页链接,保留搜索与排序
|
// PageHref 分页链接,保留搜索与排序
|
||||||
func (d HomePageData) PageHref(page int) string {
|
func (d HomePageData) PageHref(page int) string {
|
||||||
return buildFeedURL(d.ActiveBoard, d.Sort, page, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
return buildFeedURL(d.ActiveBoard, d.PermalinkSuffix, d.Sort, page, d.Keyword, d.Tag, d.Author, d.TitleOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFeedURL(boardID uint, sort string, page int, keyword, tag, author string, titleOnly bool) string {
|
func buildFeedURL(boardID uint, permalinkSuffix, sort string, page int, keyword, tag, author string, titleOnly bool) string {
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
if sort != "" && sort != "latest" {
|
if sort != "" && sort != "latest" {
|
||||||
q.Set("sort", sort)
|
q.Set("sort", sort)
|
||||||
@@ -213,7 +216,7 @@ func buildFeedURL(boardID uint, sort string, page int, keyword, tag, author stri
|
|||||||
}
|
}
|
||||||
path := "/"
|
path := "/"
|
||||||
if boardID > 0 {
|
if boardID > 0 {
|
||||||
path = fmt.Sprintf("/board/%d", boardID)
|
path = fmt.Sprintf("/board/%d%s", boardID, permalinkSuffix)
|
||||||
}
|
}
|
||||||
if enc := q.Encode(); enc != "" {
|
if enc := q.Encode(); enc != "" {
|
||||||
return path + "?" + enc
|
return path + "?" + enc
|
||||||
@@ -243,6 +246,12 @@ func (d Deps) Home(c *gin.Context) {
|
|||||||
if n, err := strconv.ParseUint(idStr, 10, 64); err == nil {
|
if n, err := strconv.ParseUint(idStr, 10, 64); err == nil {
|
||||||
boardID = uint(n)
|
boardID = uint(n)
|
||||||
}
|
}
|
||||||
|
if boardID > 0 {
|
||||||
|
match := d.permalink().MatchBoardPath(c.Request.URL.Path)
|
||||||
|
if d.redirectIfNotCanonical(c, match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome := d.chrome(ctx, "", "", "home/feed")
|
chrome := d.chrome(ctx, "", "", "home/feed")
|
||||||
@@ -309,6 +318,7 @@ func (d Deps) Home(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
posts := make([]PostListItem, 0, len(items))
|
posts := make([]PostListItem, 0, len(items))
|
||||||
|
pl := d.permalink()
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
authorName := strings.TrimSpace(it.User.Nickname)
|
authorName := strings.TrimSpace(it.User.Nickname)
|
||||||
if authorName == "" {
|
if authorName == "" {
|
||||||
@@ -319,7 +329,7 @@ func (d Deps) Home(c *gin.Context) {
|
|||||||
bname = it.Board.Name
|
bname = it.Board.Name
|
||||||
}
|
}
|
||||||
posts = append(posts, PostListItem{
|
posts = append(posts, PostListItem{
|
||||||
ID: it.ID, Title: it.Title, AuthorName: authorName, BoardName: bname,
|
ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID), AuthorName: authorName, BoardName: bname,
|
||||||
Pinned: it.Pinned, Featured: it.Featured, CommentCount: it.CommentCount,
|
Pinned: it.Pinned, Featured: it.Featured, CommentCount: it.CommentCount,
|
||||||
CreatedLabel: it.CreatedAt.Local().Format("2006-01-02 15:04"),
|
CreatedLabel: it.CreatedAt.Local().Format("2006-01-02 15:04"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ import (
|
|||||||
type PostPageData struct {
|
type PostPageData struct {
|
||||||
PageChrome
|
PageChrome
|
||||||
PostID uint
|
PostID uint
|
||||||
|
PostHref string
|
||||||
PostPath string
|
PostPath string
|
||||||
PostTitle string
|
PostTitle string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
AuthorID uint
|
AuthorID uint
|
||||||
|
AuthorHref string
|
||||||
BoardID uint
|
BoardID uint
|
||||||
|
BoardHref string
|
||||||
BoardName string
|
BoardName string
|
||||||
Pinned bool // 展示用:全局或版内置顶
|
Pinned bool // 展示用:全局或版内置顶
|
||||||
GlobalPinned bool
|
GlobalPinned bool
|
||||||
@@ -132,6 +135,10 @@ func (d Deps) PostView(c *gin.Context) {
|
|||||||
d.render404(ctx)
|
d.render404(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
match := d.permalink().MatchPostPath(c.Request.URL.Path)
|
||||||
|
if d.redirectIfNotCanonical(c, match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
post, err := d.Post.FindByID(uint(id))
|
post, err := d.Post.FindByID(uint(id))
|
||||||
if err != nil || !services.CanViewPost(post, ctx.UserID(), ctx.IsAdmin()) {
|
if err != nil || !services.CanViewPost(post, ctx.UserID(), ctx.IsAdmin()) {
|
||||||
d.render404(ctx)
|
d.render404(ctx)
|
||||||
@@ -260,9 +267,11 @@ func (d Deps) PostView(c *gin.Context) {
|
|||||||
|
|
||||||
ctx.HTML(http.StatusOK, "post", PostPageData{
|
ctx.HTML(http.StatusOK, "post", PostPageData{
|
||||||
PageChrome: chrome, PostID: post.ID,
|
PageChrome: chrome, PostID: post.ID,
|
||||||
PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)),
|
PostHref: d.postHref(post.ID),
|
||||||
|
PostPath: url.QueryEscape(d.postHref(post.ID)),
|
||||||
PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID,
|
PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID,
|
||||||
BoardID: post.BoardID, BoardName: boardName,
|
AuthorHref: d.userHref(post.UserID),
|
||||||
|
BoardID: post.BoardID, BoardHref: d.boardHref(post.BoardID), BoardName: boardName,
|
||||||
Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned,
|
Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned,
|
||||||
Featured: post.Featured, EditLocked: post.EditLocked,
|
Featured: post.Featured, EditLocked: post.EditLocked,
|
||||||
PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt),
|
PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt),
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ type userPublicData struct {
|
|||||||
// UserPublic 公开用户主页(不含邮箱)
|
// UserPublic 公开用户主页(不含邮箱)
|
||||||
func (d Deps) UserPublic(c *gin.Context) {
|
func (d Deps) UserPublic(c *gin.Context) {
|
||||||
ctx := d.ctx(c)
|
ctx := d.ctx(c)
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
idStr := stripIDParam(c.Param("id"), d.Settings.Permalink().Ext)
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
if err != nil || id == 0 {
|
if err != nil || id == 0 {
|
||||||
d.render404(ctx)
|
d.render404(ctx)
|
||||||
return
|
return
|
||||||
@@ -52,6 +53,10 @@ func (d Deps) UserPublic(c *gin.Context) {
|
|||||||
d.render404(ctx)
|
d.render404(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
match := d.permalink().MatchUserPath(c.Request.URL.Path)
|
||||||
|
if d.redirectIfNotCanonical(c, match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
st, _ := d.User.ActivityStats(user.ID)
|
st, _ := d.User.ActivityStats(user.ID)
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
@@ -67,13 +72,14 @@ func (d Deps) UserPublic(c *gin.Context) {
|
|||||||
ViewerIsAdmin: ctx.IsAdmin(),
|
ViewerIsAdmin: ctx.IsAdmin(),
|
||||||
})
|
})
|
||||||
items := make([]PostListItem, 0, len(posts))
|
items := make([]PostListItem, 0, len(posts))
|
||||||
|
pl := d.permalink()
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
board := ""
|
board := ""
|
||||||
if p.Board.ID > 0 {
|
if p.Board.ID > 0 {
|
||||||
board = p.Board.Name
|
board = p.Board.Name
|
||||||
}
|
}
|
||||||
items = append(items, PostListItem{
|
items = append(items, PostListItem{
|
||||||
ID: p.ID, Title: p.Title, AuthorName: displayName(user),
|
ID: p.ID, Title: p.Title, Href: pl.PostPath(p.ID), AuthorName: displayName(user),
|
||||||
BoardName: board, Pinned: p.Pinned || p.BoardPinned, Featured: p.Featured,
|
BoardName: board, Pinned: p.Pinned || p.BoardPinned, Featured: p.Featured,
|
||||||
CreatedLabel: formatTime(p.CreatedAt),
|
CreatedLabel: formatTime(p.CreatedAt),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -614,6 +614,73 @@ func (s *ForumSettingsService) UpdateRateLimits(post, comment, register, login,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateContentLimits 更新字数/编辑窗/分页/账号相关限制(不改 aside / 伪静态 / 限流)
|
||||||
|
func (s *ForumSettingsService) UpdateContentLimits(
|
||||||
|
postEditHours, commentEditMinutes,
|
||||||
|
titleMax, tagsMax, contentMax, commentMax,
|
||||||
|
searchMin, searchMax, pageSize,
|
||||||
|
passwordMin, avatarMB, signatureMax int,
|
||||||
|
openPostsNewTab, openLinksNewTab bool,
|
||||||
|
feedListStyle string,
|
||||||
|
) error {
|
||||||
|
updates := map[string]int{
|
||||||
|
SettingPostEditWindowHours: postEditHours,
|
||||||
|
SettingCommentEditWindowMinutes: commentEditMinutes,
|
||||||
|
SettingPostTitleMax: titleMax,
|
||||||
|
SettingPostTagsMax: tagsMax,
|
||||||
|
SettingPostContentMax: contentMax,
|
||||||
|
SettingCommentMax: commentMax,
|
||||||
|
SettingSearchKeywordMin: searchMin,
|
||||||
|
SettingSearchKeywordMax: searchMax,
|
||||||
|
SettingPageSizeDefault: pageSize,
|
||||||
|
SettingPasswordMinLen: passwordMin,
|
||||||
|
SettingAvatarMaxMB: avatarMB,
|
||||||
|
SettingSignatureMax: signatureMax,
|
||||||
|
}
|
||||||
|
if searchMax > 0 && searchMin > searchMax {
|
||||||
|
return ErrInvalidSetting
|
||||||
|
}
|
||||||
|
for key, val := range updates {
|
||||||
|
if err := s.setInt(key, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolUpdates := map[string]bool{
|
||||||
|
SettingOpenPostsInNewTab: openPostsNewTab,
|
||||||
|
SettingOpenContentLinksInNewTab: openLinksNewTab,
|
||||||
|
}
|
||||||
|
for key, on := range boolUpdates {
|
||||||
|
v := "0"
|
||||||
|
if on {
|
||||||
|
v = "1"
|
||||||
|
}
|
||||||
|
if err := s.setString(key, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style, ok := NormalizeFeedListStyle(feedListStyle)
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidSetting
|
||||||
|
}
|
||||||
|
return s.setString(SettingFeedListStyle, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePermalinkSettings 更新伪静态开关与后缀
|
||||||
|
func (s *ForumSettingsService) UpdatePermalinkSettings(enabled bool, ext string) error {
|
||||||
|
extNorm, ok := NormalizePermalinkExt(ext)
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidSetting
|
||||||
|
}
|
||||||
|
v := "0"
|
||||||
|
if enabled {
|
||||||
|
v = "1"
|
||||||
|
}
|
||||||
|
if err := s.setString(SettingPermalinkEnabled, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.setString(SettingPermalinkExt, extNorm)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
||||||
updates := map[string]int{
|
updates := map[string]int{
|
||||||
SettingPostEditWindowHours: in.PostEditWindowHours,
|
SettingPostEditWindowHours: in.PostEditWindowHours,
|
||||||
|
|||||||
@@ -94,6 +94,43 @@
|
|||||||
<button type="submit">保存限流</button>
|
<button type="submit">保存限流</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>内容限制</h2>
|
||||||
|
<p class="j13-muted">字数、编辑时间窗、分页与账号相关上限。</p>
|
||||||
|
<form method="post" action="/admin/settings/content-limits" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label>帖标题最大字数 <input name="post_title_max" type="number" min="1" value="{{.Limits.PostTitleMax}}"/></label>
|
||||||
|
<label>标签最大字数 <input name="post_tags_max" type="number" min="0" value="{{.Limits.PostTagsMax}}"/></label>
|
||||||
|
<label>帖正文最大字数 <input name="post_content_max" type="number" min="0" value="{{.Limits.PostContentMax}}"/></label>
|
||||||
|
<label>评论最大字数 <input name="comment_max" type="number" min="1" value="{{.Limits.CommentMax}}"/></label>
|
||||||
|
<label>搜索关键词最短 <input name="search_keyword_min" type="number" min="0" value="{{.Limits.SearchKeywordMin}}"/></label>
|
||||||
|
<label>搜索关键词最长 <input name="search_keyword_max" type="number" min="1" value="{{.Limits.SearchKeywordMax}}"/></label>
|
||||||
|
<label>默认分页大小 <input name="page_size_default" type="number" min="1" value="{{.Limits.PageSizeDefault}}"/></label>
|
||||||
|
<label>发帖可编辑(小时) <input name="post_edit_window_hours" type="number" min="0" value="{{.Limits.PostEditWindowHours}}"/></label>
|
||||||
|
<label>评论可编辑(分钟) <input name="comment_edit_window_minutes" type="number" min="0" value="{{.Limits.CommentEditWindowMinutes}}"/></label>
|
||||||
|
<label>密码最短长度 <input name="password_min_len" type="number" min="4" value="{{.Limits.PasswordMinLen}}"/></label>
|
||||||
|
<label>头像最大 MB <input name="avatar_max_mb" type="number" min="1" value="{{.Limits.AvatarMaxMB}}"/></label>
|
||||||
|
<label>签名最大字数 <input name="signature_max" type="number" min="0" value="{{.Limits.SignatureMax}}"/></label>
|
||||||
|
<label>Feed 列表样式
|
||||||
|
<select name="feed_list_style">
|
||||||
|
<option value="title"{{if eq .Limits.FeedListStyle "title"}} selected{{end}}>仅标题</option>
|
||||||
|
<option value="excerpt"{{if eq .Limits.FeedListStyle "excerpt"}} selected{{end}}>摘要</option>
|
||||||
|
<option value="thumbnail"{{if eq .Limits.FeedListStyle "thumbnail"}} selected{{end}}>缩略图</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="open_posts_in_new_tab" value="1"{{if .Limits.OpenPostsInNewTab}} checked{{end}}/> 帖子链接新标签打开</label>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="open_content_links_in_new_tab" value="1"{{if .Limits.OpenContentLinksInNewTab}} checked{{end}}/> 正文外链新标签打开</label>
|
||||||
|
<button type="submit">保存内容限制</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>伪静态</h2>
|
||||||
|
<p class="j13-muted">启用后公开路径形如 <code>/post/123.{{.Limits.PermalinkExt}}</code>;无后缀请求会 301 到规范 URL。</p>
|
||||||
|
<form method="post" action="/admin/settings/permalink" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="permalink_enabled" value="1"{{if .Limits.PermalinkEnabled}} checked{{end}}/> 启用伪静态后缀</label>
|
||||||
|
<label>后缀(不含点) <input name="permalink_ext" maxlength="16" pattern="[A-Za-z0-9]{1,16}" value="{{.Limits.PermalinkExt}}" placeholder="html"/></label>
|
||||||
|
<button type="submit">保存伪静态</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<h2>侧栏组件</h2>
|
<h2>侧栏组件</h2>
|
||||||
<p class="j13-muted">控制首页与帖详情右栏可选块的显示与顺序(热门帖为固定块,不在此列)。友链页「侧栏显示」与此共用同一配置。</p>
|
<p class="j13-muted">控制首页与帖详情右栏可选块的显示与顺序(热门帖为固定块,不在此列)。友链页「侧栏显示」与此共用同一配置。</p>
|
||||||
<form method="post" action="/admin/settings/aside-widgets" class="j13-form j13-admin-form">
|
<form method="post" action="/admin/settings/aside-widgets" class="j13-form j13-admin-form">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<ul class="j13-boards__grid">
|
<ul class="j13-boards__grid">
|
||||||
{{range .Items}}
|
{{range .Items}}
|
||||||
<li>
|
<li>
|
||||||
<a class="j13-boards__card" href="/board/{{.ID}}" data-color="{{.ColorIndex}}">
|
<a class="j13-boards__card" href="{{.Href}}" data-color="{{.ColorIndex}}">
|
||||||
<span class="j13-boards__dot" aria-hidden="true"></span>
|
<span class="j13-boards__dot" aria-hidden="true"></span>
|
||||||
<span class="j13-boards__body">
|
<span class="j13-boards__body">
|
||||||
<strong>{{.Name}}</strong>
|
<strong>{{.Name}}</strong>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<ul class="j13-post-list">
|
<ul class="j13-post-list">
|
||||||
{{range .Items}}
|
{{range .Items}}
|
||||||
<li class="j13-post-item">
|
<li class="j13-post-item">
|
||||||
<a class="j13-post-item__title" href="/post/{{.PostID}}">{{.Title}}</a>
|
<a class="j13-post-item__title" href="{{.Href}}"{{if $.OpenPostsInNewTab}} target="_blank" rel="noopener"{{end}}>{{.Title}}</a>
|
||||||
<div class="j13-post-item__meta">
|
<div class="j13-post-item__meta">
|
||||||
{{if .AuthorName}}<span>{{.AuthorName}}</span>{{end}}
|
{{if .AuthorName}}<span>{{.AuthorName}}</span>{{end}}
|
||||||
{{if .BoardName}}<span>{{.BoardName}}</span>{{end}}
|
{{if .BoardName}}<span>{{.BoardName}}</span>{{end}}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<ul class="j13-post-list">
|
<ul class="j13-post-list">
|
||||||
{{range .Posts}}
|
{{range .Posts}}
|
||||||
<li class="j13-post-item">
|
<li class="j13-post-item">
|
||||||
<a class="j13-post-item__title" href="{{postURL .ID}}">{{.Title}}</a>
|
<a class="j13-post-item__title" href="{{.Href}}"{{if $.OpenPostsInNewTab}} target="_blank" rel="noopener"{{end}}>{{.Title}}</a>
|
||||||
<div class="j13-post-item__meta">
|
<div class="j13-post-item__meta">
|
||||||
{{if .Pinned}}<span class="j13-tag j13-tag--pin">置顶</span>{{end}}
|
{{if .Pinned}}<span class="j13-tag j13-tag--pin">置顶</span>{{end}}
|
||||||
{{if .Featured}}<span class="j13-tag j13-tag--feat">精华</span>{{end}}
|
{{if .Featured}}<span class="j13-tag j13-tag--feat">精华</span>{{end}}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
{{if .IsQuestion}}
|
{{if .IsQuestion}}
|
||||||
{{if .QuestionResolved}}<span class="j13-tag j13-tag--resolved">已解决</span>{{else}}<span class="j13-tag j13-tag--unresolved">未解决</span>{{end}}
|
{{if .QuestionResolved}}<span class="j13-tag j13-tag--resolved">已解决</span>{{else}}<span class="j13-tag j13-tag--unresolved">未解决</span>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .BoardName}}<a class="j13-tag" href="/board/{{.BoardID}}">{{.BoardName}}</a>{{end}}
|
{{if .BoardName}}<a class="j13-tag" href="{{.BoardHref}}">{{.BoardName}}</a>{{end}}
|
||||||
{{if .AuthorID}}<a href="/user/{{.AuthorID}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}}
|
{{if .AuthorID}}<a href="{{.AuthorHref}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}}
|
||||||
<span>{{.CreatedLabel}}</span>
|
<span>{{.CreatedLabel}}</span>
|
||||||
<span>{{.ViewCount}} 阅读</span>
|
<span>{{.ViewCount}} 阅读</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
<button type="submit">{{if .Favorited}}取消收藏{{else}}收藏{{end}}</button>
|
<button type="submit">{{if .Favorited}}取消收藏{{else}}收藏{{end}}</button>
|
||||||
</form>
|
</form>
|
||||||
{{if .CanEdit}}<a class="j13-linkbtn" href="/post/{{.PostID}}/edit">编辑</a>{{end}}
|
{{if .CanEdit}}<a class="j13-linkbtn" href="{{.PostHref}}/edit">编辑</a>{{end}}
|
||||||
{{if .CanViewRevisions}}<a class="j13-linkbtn" href="/post/{{.PostID}}/revisions">修订历史</a>{{end}}
|
{{if .CanViewRevisions}}<a class="j13-linkbtn" href="{{.PostHref}}/revisions">修订历史</a>{{end}}
|
||||||
{{if .CanReportPost}}
|
{{if .CanReportPost}}
|
||||||
<details class="j13-report">
|
<details class="j13-report">
|
||||||
<summary class="j13-linkbtn">举报</summary>
|
<summary class="j13-linkbtn">举报</summary>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<a class="j13-aside__link{{if eq .Path "/boards"}} is-active{{end}}" href="/boards">全部板块</a>
|
<a class="j13-aside__link{{if eq .Path "/boards"}} is-active{{end}}" href="/boards">全部板块</a>
|
||||||
<a class="j13-aside__link{{if and (eq .ActiveBoard 0) (ne .Path "/boards")}} is-active{{end}}" href="/">全部帖子</a>
|
<a class="j13-aside__link{{if and (eq .ActiveBoard 0) (ne .Path "/boards")}} is-active{{end}}" href="/">全部帖子</a>
|
||||||
{{range .Boards}}
|
{{range .Boards}}
|
||||||
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="/board/{{.ID}}">{{.Name}}</a>
|
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="{{.Href}}">{{.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</aside>
|
</aside>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
{{if .RightAside.HotPosts}}
|
{{if .RightAside.HotPosts}}
|
||||||
<ul class="j13-widget__list">
|
<ul class="j13-widget__list">
|
||||||
{{range .RightAside.HotPosts}}
|
{{range .RightAside.HotPosts}}
|
||||||
<li><a href="{{postURL .ID}}">{{.Title}}</a></li>
|
<li><a href="{{.Href}}">{{.Title}}</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<ul class="j13-widget__list">
|
<ul class="j13-widget__list">
|
||||||
{{range .Comments}}
|
{{range .Comments}}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{postURL .PostID}}">{{.Excerpt}}</a>
|
<a href="{{.PostHref}}">{{.Excerpt}}</a>
|
||||||
<span class="j13-widget__meta">{{.Author}} · {{.PostTitle}}</span>
|
<span class="j13-widget__meta">{{.Author}} · {{.PostTitle}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{{if .Users}}
|
{{if .Users}}
|
||||||
<ul class="j13-widget__list">
|
<ul class="j13-widget__list">
|
||||||
{{range .Users}}
|
{{range .Users}}
|
||||||
<li><a href="/user/{{.ID}}">{{.Nickname}}</a></li>
|
<li><a href="{{.Href}}">{{.Nickname}}</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<ul class="j13-post-list">
|
<ul class="j13-post-list">
|
||||||
{{range .Posts}}
|
{{range .Posts}}
|
||||||
<li class="j13-post-item">
|
<li class="j13-post-item">
|
||||||
<a class="j13-post-item__title" href="/post/{{.ID}}">{{.Title}}</a>
|
<a class="j13-post-item__title" href="{{.Href}}"{{if $.OpenPostsInNewTab}} target="_blank" rel="noopener"{{end}}>{{.Title}}</a>
|
||||||
<div class="j13-post-item__meta">
|
<div class="j13-post-item__meta">
|
||||||
{{if .BoardName}}<span>{{.BoardName}}</span>{{end}}
|
{{if .BoardName}}<span>{{.BoardName}}</span>{{end}}
|
||||||
<span>{{.CreatedLabel}}</span>
|
<span>{{.CreatedLabel}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user