diff --git a/.workbuddy/memory/2026-09-10.md b/.workbuddy/memory/2026-09-10.md
index 9dfdd86..86852c0 100644
--- a/.workbuddy/memory/2026-09-10.md
+++ b/.workbuddy/memory/2026-09-10.md
@@ -333,3 +333,51 @@ RAM 子账号授予 `AliyunYundunGreenWebFullAccess`;②CHANGELOG 条目仍未
- 沙箱内若要用 overlay,注意 **overlay.json 的 Replace 键盘符必须小写**(`c:/...`),
大写 `C:/...` 会被 Go 规范化成 `c:\` 后匹配不上、报 `deleted in overlay`;stub 在
`dev/s3stub/s3_stub.go`。本次用 `C:/Users/Tamakyi Gekka/AppData/Local/Temp/tb-overlay2.json` 跑通。
+
+---
+
+## 第六轮:设置面板聚合(新增「防护设置」tab)+ 审核严格度排版修复
+
+用户诉求:① 把防骚扰、屏蔽词、审核严格度、提问密码聚合到 `/user/profile` 下一个独立 tab;
+② 审核严格度档位处「一个字就换行」很难看。
+
+### 决策(AskUserQuestion 已确认)
+
+- 新 tab 名称:**防护设置**(i18n 键 `admin.tabSecurity`,tab id `security`)。
+- **「页面访问密码」一并迁入**(原先在「用户设置」面板)。
+- 旧位置**完全移除,不留跳转提示**。
+
+### 改动
+
+**后端**
+- `route/user/features.go`:新增 `ProfileTabSecurity = "security"`,并纳入 `ProfileTab()` 白名单
+ (不加入的话回跳会落到 account 面板)。
+- `route/user/password.go`:`UpdateAskPassword` / `UpdatePagePassword` 的硬编码回跳
+ (`profileURLWithTab(ProfileTabQuestion)`、`"/user/profile"`)全部改为 `returnTab(ctx)`,
+ 跟随表单携带的 `?tab=`。
+
+**模板**
+- `templates/user/profile.html`:tab 栏新增「防护设置」按钮;新增
+ `
`,内含 防骚扰 / 屏蔽词 /
+ 审核严格度 / 页面访问密码 / 提问密码 五个表单(action 均带 `?tab=security`)。
+ 提问/私聊/投票面板中这些表单全部删除(**原先屏蔽词与审核严格度被重复渲染 3 份**)。
+ account 面板移除「页面访问密码」卡(原编号 7 让位,账号操作改为 7)。
+ JS 面板切换是全泛型(`[data-profile-tab]` / `[data-profile-panel]`),无需改动。
+- `templates/user/moderation-strictness.html`(**只用 shell/脚本改,禁用 Write/Edit**):
+ 1. 档位表格:档位列 `uk-table-shrink` + `white-space:nowrap`、范围列 `uk-table-expand`
+ —— 修掉右侧长文案把 4 字档位名(畅所欲言/正常水平/不听坏话)挤成逐字折行。
+ 2. 选择区改三行:标题(显式 `display:block`)→ 下拉(独立 div)→
+ 「未设置时跟随站点默认档位。」(独立 div)。
+
+**i18n**:四语字典新增 `admin.tabSecurity`(防护设置 / 防護設置 / Protection / 防護設定),
+插在「功能管理」组之后的**独立组**,未切开任何既有对齐组(避免 gofmt 重排污染 diff)。
+
+### 验证
+
+- `go test ./templates/`(含 profile 渲染、严格度回显断言 `selectedStrictnessOptions`)ok;
+ `./internal/i18n/` ok(四语键一致)。
+- `go build` + `go vet` `./route/user/` 均 0。
+- ⚠️ **gofmt 提醒(重要,易误判)**:`gofmt -l` 会列出**所有 CRLF 文件**(含本次完全没碰的
+ `route/user/moderation.go`、`vote.go`),因为 gofmt 把 CRLF 规范成 LF。
+ 剥离 CR 后比对,本次改动**零格式差异** → 格式是对的。
+ **切勿跑 `gofmt -w`**,那会把文件转成 LF、造成全量 diff 污染。
diff --git a/internal/i18n/dict_en.go b/internal/i18n/dict_en.go
index b500914..c6784a1 100644
--- a/internal/i18n/dict_en.go
+++ b/internal/i18n/dict_en.go
@@ -634,6 +634,9 @@ var enDict = map[string]string{
"admin.featureResetQuota": "Reset counter",
"admin.featureSave": "Save this feature",
+ // Protection panel: aggregates anti-harassment / blocked words / moderation strictness / page password / ask password
+ "admin.tabSecurity": "Protection",
+
// Feature management: states and messages
"feature.name.question": "Questions",
"feature.name.chat": "Chats",
diff --git a/internal/i18n/dict_ja.go b/internal/i18n/dict_ja.go
index 799f8d7..5968abd 100644
--- a/internal/i18n/dict_ja.go
+++ b/internal/i18n/dict_ja.go
@@ -634,6 +634,9 @@ var jaDict = map[string]string{
"admin.featureResetQuota": "カウントをリセット",
"admin.featureSave": "この機能を保存",
+ // コンテンツ保護パネル:防骚扰 / 屏蔽詞 / 審核の厳格度 / ページアクセスパスワード / 質問パスワード を集約
+ "admin.tabSecurity": "防護設定",
+
// 機能管理:状態とメッセージ
"feature.name.question": "質問",
"feature.name.chat": "チャット",
diff --git a/internal/i18n/dict_zh_cn.go b/internal/i18n/dict_zh_cn.go
index 0828df6..166ec01 100644
--- a/internal/i18n/dict_zh_cn.go
+++ b/internal/i18n/dict_zh_cn.go
@@ -634,6 +634,9 @@ var zhCNDict = map[string]string{
"admin.featureResetQuota": "重置计数",
"admin.featureSave": "保存该功能设置",
+ // 内容防护面板:聚合防骚扰 / 屏蔽词 / 审核严格度 / 页面访问密码 / 提问密码
+ "admin.tabSecurity": "防护设置",
+
// 功能管理:状态与提示
"feature.name.question": "提问",
"feature.name.chat": "私聊",
diff --git a/internal/i18n/dict_zh_tw.go b/internal/i18n/dict_zh_tw.go
index cfe942b..8575d31 100644
--- a/internal/i18n/dict_zh_tw.go
+++ b/internal/i18n/dict_zh_tw.go
@@ -634,6 +634,9 @@ var zhTWDict = map[string]string{
"admin.featureResetQuota": "重設計數",
"admin.featureSave": "儲存該功能設定",
+ // 內容防護面板:聚合防騷擾 / 屏蔽詞 / 審核嚴格度 / 頁面訪問密碼 / 提問密碼
+ "admin.tabSecurity": "防護設置",
+
// 功能管理:狀態與提示
"feature.name.question": "提問",
"feature.name.chat": "私聊",
diff --git a/route/user/features.go b/route/user/features.go
index 4ec471b..38bbbf8 100644
--- a/route/user/features.go
+++ b/route/user/features.go
@@ -28,6 +28,10 @@ const (
ProfileTabQuestion = db.FeatureQuestion
ProfileTabChat = db.FeatureChat
ProfileTabVote = db.FeatureVote
+ // ProfileTabSecurity 与受控功能无关:聚合「内容防护」类设置
+ // (防骚扰、屏蔽词、审核严格度、页面访问密码、提问密码),
+ // 这些设置横跨多个功能,原先散落在提问/私聊/投票面板(屏蔽词与审核严格度还被重复渲染 3 份)。
+ ProfileTabSecurity = "security"
)
// profileURLWithTab 账号设置页 + 指定面板的回跳地址,用于保存后停留在原面板。
@@ -38,7 +42,7 @@ func profileURLWithTab(tab string) string {
// ProfileTab 归一化账号设置页的 tab 参数,非法值回落到用户设置面板。
func ProfileTab(tab string) string {
switch tab {
- case ProfileTabQuestion, ProfileTabChat, ProfileTabVote:
+ case ProfileTabQuestion, ProfileTabChat, ProfileTabVote, ProfileTabSecurity:
return tab
}
return ProfileTabAccount
diff --git a/route/user/password.go b/route/user/password.go
index 9fb0aab..8466e33 100644
--- a/route/user/password.go
+++ b/route/user/password.go
@@ -17,7 +17,8 @@ import (
func UpdatePagePassword(ctx context.Context, f form.UpdatePagePassword) {
// 反序列化失败(如请求体超限)时零值 Password="" 会把密码静默解除——先中止,
// 不触碰现有密码。govalid 校验失败(如密码超长)同样在此拦截,避免绕过前端限制。
- if abortOnFormError(ctx, "/user/profile") {
+ // 该表单已随「内容防护」聚合迁入 security 面板,回跳地址跟随表单携带的 ?tab=。
+ if abortOnFormError(ctx, returnTab(ctx)) {
return
}
@@ -31,13 +32,14 @@ func UpdatePagePassword(ctx context.Context, f form.UpdatePagePassword) {
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.pagePwdUpdated"))
}
}
- ctx.Redirect("/user/profile")
+ ctx.Redirect(returnTab(ctx))
}
// UpdateAskPassword 更新提问密码
func UpdateAskPassword(ctx context.Context, f form.UpdateAskPassword) {
// 同上:反序列化失败或校验失败时零值 Password="" 会把提问密码静默解除。
- if abortOnFormError(ctx, profileURLWithTab(ProfileTabQuestion)) {
+ // 回跳地址跟随表单携带的 ?tab=(该表单已迁入「内容防护」security 面板)。
+ if abortOnFormError(ctx, returnTab(ctx)) {
return
}
@@ -51,6 +53,6 @@ func UpdateAskPassword(ctx context.Context, f form.UpdateAskPassword) {
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.askPwdUpdated"))
}
}
- // 提问密码属于提问功能的专属设置,保存后回到「提问」面板
- ctx.Redirect(profileURLWithTab(ProfileTabQuestion))
+ // 保存后回到提交时所在的面板(内容防护面板)
+ ctx.Redirect(returnTab(ctx))
}
diff --git a/templates/user/moderation-strictness.html b/templates/user/moderation-strictness.html
index 7e227ef..c886b91 100644
--- a/templates/user/moderation-strictness.html
+++ b/templates/user/moderation-strictness.html
@@ -8,59 +8,65 @@
本模板只负责展示,不重复实现判定。
-->
{{ $root := .Root }}
-