Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7eb4ce7
优化长文沉浸阅读样式与版式设置
HereIsYui Jul 18, 2026
7ae0b42
隐藏长文页面顶部和底部导航
HereIsYui Jul 18, 2026
23cb2d1
长文模板移除页面导航视觉内容
HereIsYui Jul 18, 2026
92c25f9
对齐长文阅读页鱼乎配色并修复顶部留白
HereIsYui Jul 18, 2026
adcdb30
长文评论改为侧滑阅读面板
HereIsYui Jul 18, 2026
0148e08
修复长文评论编辑器并居中展开面板
HereIsYui Jul 18, 2026
d7c9b2b
修复长文评论面板纵向错位
HereIsYui Jul 18, 2026
e5c5f0c
收窄长文评论面板并修正工具栏位置
HereIsYui Jul 18, 2026
b484624
修正长文评论面板滚动条偏移
HereIsYui Jul 18, 2026
04b8e01
收窄长文评论区并避让阅读工具栏
HereIsYui Jul 19, 2026
ac68013
改进长文阅读工具栏
HereIsYui Jul 19, 2026
9b7e616
进一步收窄长文评论区
HereIsYui Jul 19, 2026
bcf262d
清除长文评论区最小宽度
HereIsYui Jul 19, 2026
30753e5
调整长文评论区宽度为320像素
HereIsYui Jul 19, 2026
ebec516
扩展长文评论输入区域
HereIsYui Jul 19, 2026
0ac5072
修正长文窄版排版与顶部动画
HereIsYui Jul 19, 2026
1bf659c
增加长文返回首页入口
HereIsYui Jul 19, 2026
104c1d3
实现长篇段评与段落锚点迁移
HereIsYui Jul 19, 2026
e98f295
修复长文段落拆分异常
HereIsYui Jul 19, 2026
9586d35
修正长文段评位置和滚动条
HereIsYui Jul 19, 2026
c412c63
优化长文段评交互样式
HereIsYui Jul 19, 2026
6b1a416
调整段评切换与标记对齐
HereIsYui Jul 20, 2026
3ebb3a6
优化长文段评标记与工具栏位置
HereIsYui Jul 20, 2026
f451f16
调整段评标记显示方式
HereIsYui Jul 20, 2026
bfd9a23
简化长文段评回复展示
HereIsYui Jul 20, 2026
9641c6d
优化长文段评评论操作
HereIsYui Jul 20, 2026
a474c75
简化长文段评评论操作
HereIsYui Jul 20, 2026
dfd88ec
修复长文段评操作悬停
HereIsYui Jul 20, 2026
e3e1e42
优化长文段评选中样式
HereIsYui Jul 20, 2026
83d4556
加深长文段评选中高亮
HereIsYui Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
- 首页两列对齐约束:`getIndexRecentArticles` 的第一页会插入全部置顶且不截断;第二页起需按第一页“置顶占位数”补偿 `fetchSize` 与分页偏移,并基于已排除置顶 ID 的普通文章序列取数,保证两列等高、不重复、不丢中间文章。
- 首页右侧排行补偿(无前端延迟):由 `IndexProcessor#loadIndexData` 按两列最新文章的最大行数计算 `rankCompensateRows`,先换算“右栏总补偿行数”再分摊到 `checkinVisibleCount/onlineVisibleCount`,Freemarker 直接按该数量渲染,不再依赖 JS 运行时增删行。
- 专栏封面链路:封面字段是 `long_article_column.columnCoverURL`;默认封面在 `LongArticleColumnQueryService` 填充;作者管理页为 `/column/manage`,编辑长篇页的弹窗脚本是 `long-article-cover-dialog.js`,新建长篇封面随 `/article` 请求的 `columnCoverURL` 写入;封面保存接口为 `POST /api/columns/{columnId}/cover`,处理器是 `LongArticleColumnProcessor`。
- 长文阅读页以 `articleType=6` 区分,PC/移动模板均为 `skins/classic/*/article.ftl`,样式在 `index.scss`/`mobile-base.scss`,交互在 `long-article.js`/`m-long-article.js`;模板通过 `hidePageChrome` 不渲染顶部与底部视觉内容,但 footer 的公共脚本仍需加载;桌面宽度和两端字号统一保存在 `localStorage.longArticleSettings`,宽度仅在 PC 生效;PC 评论按钮控制居中的“正文 + 评论”阅读舞台,移动端使用右侧抽屉,现有评论列表、筛选、分页和编辑器均复用。
- 长文段评链路:`LongArticleParagraphService` 根据服务端清洗后的正文生成段落锚点并给 HTML 添加 `data-long-paragraph-*`;段评字段存于 `comment`(`commentType=1`),新增接口位于 `CommentProcessor` 的 `/comment/paragraph*`;评论编辑事务调用段落迁移,无法匹配的段评标记为失效但保留快照。PC/移动均加载 `long-article-paragraph.js`,段评列表复用现有评论线程与编辑器能力。
- 管理员文章页编辑长文专栏时,`admin/article.ftl` 会随文章表单提交 `columnCoverURL`,后端在 `ArticleMgmtService` 的文章事务内复用 `ColumnCoverMgmtService` 校验并更新或清空专栏封面。
- 路由总入口:`Router#requestMapping` + 各 Processor `register()`;新增路由先决定使用 `loginCheck` / `apiCheck` / `permission` / `anonymousViewCheck` 哪条链路。
- Latke 路由存在静态段被相邻动态段截获的风险(如 `/article/{id}/revisions/list` 可能进入 `/article/{id}/revisions/{revisionId}`);新增相邻路由时需避免路径歧义,或在处理方法中显式识别保留字。
Expand Down
101 changes: 101 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2318,6 +2318,107 @@ curl --location --request POST 'https://fishpi.cn/comment/thread/replies' \
| commentContent | 评论原文(Markdown格式) | hello world |
| commentOriginalCommentId | (选填)如果是回复某个评论,传递此参数并填写值为要回复的评论的oId | 1646216549324 |

### 长篇段评

`POST /comment/paragraph`

为长篇正文指定段落发表评论或回复。旧的 `POST /comment` 继续创建章评。

请求:

| Key | 说明 | 示例 |
| --- | --- | --- |
| apiKey | 通用密钥,可选 | oXTQTD4ljryXoIxa1lySgEl6aObrIhSS |
| articleId | 长篇文章 Id | 1645002736006 |
| paragraphId | 服务端生成的段落 Id | `64 位小写 SHA-256` |
| commentContent | 评论原文(Markdown 格式) | 这一段写得很好 |
| commentAnonymous | 是否匿名评论 | false |
| commentVisible | 是否仅楼主可见 | false |
| commentOriginalCommentId | 回复时填写段评 Id,可选 | 1646216549324 |

请求示例:

```bash
curl -X POST "https://fishpi.cn/comment/paragraph" \
-H "Content-Type: application/json" \
-d '{"apiKey":"你的 apiKey","articleId":"1645002736006","paragraphId":"64 位小写 SHA-256","commentContent":"这一段写得很好","commentAnonymous":false,"commentVisible":false}'
```

响应:

| Key | 说明 | 示例 |
| --- | --- | --- |
| code | `0` 表示成功 | 0 |
| commentId | 新评论 Id | 1646216549324 |

> 仅支持 `articleType=6`。段落类型、顺序和文本快照由服务端校验并保存,客户端不能伪造。新增根评论必须关联当前有效段落;回复必须属于同一文章和同一段评线程,原段落已修改的线程仍可继续回复。

### 获取长篇段评摘要

`POST /comment/paragraph/summary`

获取长篇当前有效段落的评论数量,数量包含根评论和回复。

请求:

| Key | 说明 | 示例 |
| --- | --- | --- |
| apiKey | 访问受限文章时填写 | oXTQTD4ljryXoIxa1lySgEl6aObrIhSS |
| articleId | 长篇文章 Id | 1645002736006 |

请求示例:

```bash
curl -X POST "https://fishpi.cn/comment/paragraph/summary" \
-H "Content-Type: application/json" \
-d '{"articleId":"1645002736006"}'
```

响应:

| Key | 说明 | 示例 |
| --- | --- | --- |
| code | `0` 表示成功 | 0 |
| paragraphComments | 段落评论数量 | `[...]` |
| - paragraphId | 段落 Id | `64 位小写 SHA-256` |
| - commentCount | 段评总数 | 3 |

### 获取长篇段评线程

`POST /comment/paragraph/thread/parents`

获取指定段落的评论线程根评论。

请求:

| Key | 说明 | 示例 |
| --- | --- | --- |
| apiKey | 访问受限文章时填写 | oXTQTD4ljryXoIxa1lySgEl6aObrIhSS |
| articleId | 长篇文章 Id | 1645002736006 |
| paragraphId | 段落 Id | `64 位小写 SHA-256` |
| paginationCurrentPageNum | 页码,默认 1 | 1 |
| sort | `hot` 表示热门排序 | hot |

请求示例:

```bash
curl -X POST "https://fishpi.cn/comment/paragraph/thread/parents" \
-H "Content-Type: application/json" \
-d '{"articleId":"1645002736006","paragraphId":"64 位小写 SHA-256","paginationCurrentPageNum":1,"sort":"hot"}'
```

响应:

| Key | 说明 | 示例 |
| --- | --- | --- |
| code | `0` 表示成功 | 0 |
| commentThreadParents | 段评线程根评论,字段与线程接口一致 | `[...]` |
| paragraphId | 当前段落 Id | `64 位小写 SHA-256` |
| paragraphSnapshot | 当前段落摘要 | 这一段写得很好 |
| pagination | 分页信息 | `{...}` |

> 段评接口遵循文章匿名访问、讨论帖权限和评论可见性校验;响应不返回评论 IP、UA、联系方式、密钥或完整用户对象。文章编辑后无法匹配的段评会保留为“原段落已修改”,不会丢失。

### 更新评论

`PUT /comment/{评论oId}`
Expand Down
9 changes: 9 additions & 0 deletions sql/20260719_long_article_paragraph_comment.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ALTER TABLE `symphony_comment`
ADD COLUMN `commentType` INT NOT NULL DEFAULT 0,
ADD COLUMN `commentParagraphId` VARCHAR(64) NOT NULL DEFAULT '',
ADD COLUMN `commentParagraphKind` VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN `commentParagraphIndex` INT NOT NULL DEFAULT -1,
ADD COLUMN `commentParagraphSnapshot` VARCHAR(1024) NOT NULL DEFAULT '',
ADD COLUMN `commentParagraphStatus` INT NOT NULL DEFAULT 0,
ADD INDEX `idx_cmt_article_type_paragraph_status_oid`
(`commentOnArticleId`, `commentType`, `commentParagraphId`, `commentParagraphStatus`, `commentCreateTime`, `oId`);
20 changes: 20 additions & 0 deletions src/main/java/org/b3log/symphony/model/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ public final class Comment {
*/
public static final String COMMENT_VISIBLE = "commentVisible";

public static final String COMMENT_TYPE = "commentType";

public static final String COMMENT_PARAGRAPH_ID = "commentParagraphId";

public static final String COMMENT_PARAGRAPH_KIND = "commentParagraphKind";

public static final String COMMENT_PARAGRAPH_INDEX = "commentParagraphIndex";

public static final String COMMENT_PARAGRAPH_SNAPSHOT = "commentParagraphSnapshot";

public static final String COMMENT_PARAGRAPH_STATUS = "commentParagraphStatus";

//// Transient ////
/**
* Key of comment revision count.
Expand Down Expand Up @@ -292,6 +304,14 @@ public final class Comment {
*/
public static final int COMMENT_VISIBLE_C_AUTHOR = 1;

public static final int COMMENT_TYPE_C_ARTICLE = 0;

public static final int COMMENT_TYPE_C_PARAGRAPH = 1;

public static final int COMMENT_PARAGRAPH_STATUS_C_ACTIVE = 0;

public static final int COMMENT_PARAGRAPH_STATUS_C_ORPHANED = 1;

//// Validation constants
/**
* Max comment content length.
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/b3log/symphony/processor/ArticleProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ public class ArticleProcessor {
@Inject
private LongArticleColumnQueryService longArticleColumnQueryService;

@Inject
private LongArticleParagraphService longArticleParagraphService;

/**
* Comment query service.
*/
Expand Down Expand Up @@ -534,6 +537,9 @@ public void showArticleApi(final RequestContext context) {
articleQueryService.processArticleContent(article);

if (Article.ARTICLE_TYPE_C_LONG == article.optInt(Article.ARTICLE_TYPE)) {
final JSONObject paragraphContent = longArticleParagraphService.annotateRenderedContent(
articleId, article.optString(Article.ARTICLE_CONTENT));
article.put(Article.ARTICLE_CONTENT, paragraphContent.optString("html"));
final JSONObject readStat = longArticleReadService.getStat(articleId);
article.put("longArticleReadStat", readStat);
dataModel.put("longArticleReadStat", readStat);
Expand Down Expand Up @@ -1418,6 +1424,9 @@ public void showArticle(final RequestContext context) {
articleQueryService.processArticleContent(article);

if (Article.ARTICLE_TYPE_C_LONG == article.optInt(Article.ARTICLE_TYPE)) {
final JSONObject paragraphContent = longArticleParagraphService.annotateRenderedContent(
articleId, article.optString(Article.ARTICLE_CONTENT));
article.put(Article.ARTICLE_CONTENT, paragraphContent.optString("html"));
final JSONObject readStat = longArticleReadService.getStat(articleId);
article.put("longArticleReadStat", readStat);
dataModel.put("longArticleReadStat", readStat);
Expand Down Expand Up @@ -1640,6 +1649,11 @@ public void showArticle(final RequestContext context) {
// Load comments
final List<JSONObject> articleComments = commentQueryService.getArticleThreadParentComments(commentThreadOptions);
article.put(Article.ARTICLE_T_COMMENTS, (Object) articleComments);
if (Article.ARTICLE_TYPE_C_LONG == article.optInt(Article.ARTICLE_TYPE)) {
article.put("articleOrphanedParagraphComments", (Object)
commentQueryService.getOrphanedParagraphThreadParentComments(
articleId, currentUserId, articleAuthorId));
}
article.put("commentors", (Object) commentQueryService.getArticleCommentors(articleId));

// Fill comment thank
Expand Down
Loading
Loading