功能概述与设计理念
段内评论系统是一种创新的交互功能,它允许用户选中任意文本段落进行精准评论,实现了类似现代文档协作工具的交互体验。与传统的整页评论不同,段内评论能够精确定位到具体的文本内容,为用户提供更加细致的讨论空间。
核心设计思路:
- 基于文本选择的精确评论定位
- 支持选中任意长度的文本段落
- 评论内容与原文本段落关联存储
- 实时高亮显示评论区域
- 支持评论回复与盖楼机制
技术架构与实现原理
整体架构
段内评论功能采用前后端分离的实现方式,主要包括以下组件:
- 前端交互层:负责文本选择监听、评论弹窗、高亮显示等用户交互
- 后端API层:提供评论的增删改查等RESTful接口
- 数据存储层:使用MySQL存储评论数据,支持关联查询
graph TD
subgraph 前端交互层
A[用户浏览器]
B[文本选择监听]
C[评论弹窗]
D[高亮显示]
E[侧边抽屉]
end
subgraph 后端API层
F[RESTful API]
G[评论管理]
H[权限控制]
end
subgraph 数据存储层
I[MySQL数据库]
J[评论表]
K[用户表]
end
A --> B
B --> C
C --> F
F --> G
G --> I
I --> J
I --> K
G --> H
H --> F
F --> D
D --> E
核心流程
- 文本选择:用户在页面上选中任意文本
- 触发评论:系统显示评论工具提示,用户点击后弹出评论模态框
- 提交评论:用户输入评论内容并提交
- 数据存储:后端接收评论数据并存储到数据库
- 前端更新:刷新评论列表并高亮显示评论区域
flowchart TD
A[用户选择文本] --> B[显示评论工具提示]
B --> C[点击评论按钮]
C --> D[弹出评论模态框]
D --> E[用户输入评论内容]
E --> F[提交评论]
F --> G[前端发送API请求]
G --> H[后端接收请求]
H --> I[验证用户权限]
I --> J[存储评论数据]
J --> K[返回成功响应]
K --> L[前端关闭模态框]
L --> M[刷新评论列表]
M --> N[高亮显示评论区域]
前端实现细节
核心交互逻辑
前端实现主要集中在页面文件中,包含以下关键功能:
// 文本选择事件监听
document.addEventListener('selectionchange', function() {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText.length > 0) {
// 显示评论工具提示
showInlineCommentTooltip(selection);
currentSelectionText = selectedText;
} else {
// 隐藏工具提示
hideInlineCommentTooltip();
currentSelectionText = "";
}
});
// 显示评论弹窗
function showInlineCommentModal() {
if (!currentSelectionText) return;
document.getElementById('inline-comment-selected-text').innerText = currentSelectionText;
document.getElementById('inline-comment-modal').style.display = 'flex';
document.getElementById('inline-comment-tooltip').style.display = 'none';
// 重置输入框
document.getElementById('inline-comment-input').value = "";
}
// 提交段内评论
async function submitInlineComment() {
const content = document.getElementById('inline-comment-input').value;
if (!content) return;
try {
const response = await fetch('/api/inline_comment/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
post_id: currentPostId,
content: content,
selected_text: currentSelectionText
})
});
const res = await response.json();
if (res.status === 0) {
closeInlineCommentModal();
loadInlineComments(); // 刷新高亮
alert('评论发布成功');
} else if (res.status === 401) {
alert('请先登录');
} else {
alert('发布失败: ' + res.reason);
}
} catch (e) {
console.error(e);
alert('发布失败,请稍后重试');
}
}
评论高亮与展示
前端实现了评论区域的高亮显示和侧边抽屉式评论展示:
function highlightInlineComments(comments) {
// 清除现有高亮
const content = document.getElementById('post-markdown-content');
const existingHighlights = content.querySelectorAll('.inline-comment-target');
existingHighlights.forEach(el => {
const text = el.textContent;
el.outerHTML = text;
});
// 为每个选中的文本添加高亮
const text = content.textContent;
let lastIndex = 0;
let result = '';
// 简单实现:查找文本并添加高亮
// 注意:实际实现中需要考虑文本重复的情况
comments.forEach(comment => {
const selectedText = comment.selected_text;
const index = text.indexOf(selectedText, lastIndex);
if (index !== -1) {
result += text.substring(lastIndex, index);
result += `<span class="inline-comment-target" onclick="showCommentDrawer('${selectedText}')">${selectedText}</span>`;
lastIndex = index + selectedText.length;
}
});
result += text.substring(lastIndex);
content.innerHTML = result;
}
function showCommentDrawer(text) {
const drawer = document.getElementById('comment-drawer');
const backdrop = document.getElementById('drawer-backdrop');
const body = document.getElementById('drawer-body');
// 过滤此文本的评论
const comments = allInlineComments.filter(c => c.selected_text === text);
const commentTree = buildCommentTree(comments);
body.innerHTML = `
<div class="selected-text-quote">"${escapeHtml(text)}"</div>
<div class="comments-container">
${commentTree.length > 0 ? commentTree.map(c => renderCommentNode(c)).join('') : '<div style="color:var(--text-secondary); text-align:center; padding:20px;">暂无评论</div>'}
</div>
`;
drawer.classList.add('visible');
backdrop.classList.add('visible');
}
样式设计
CSS样式定义包括评论工具提示、高亮样式和侧边抽屉:
/* Inline Comment Tooltip Arrow */
.inline-comment-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.inline-comment-target {
border-bottom: 1px dashed #999;
cursor: pointer;
position: relative;
}
.inline-comment-target:hover {
border-bottom-color: #2cbb5d;
background-color: rgba(44, 187, 93, 0.1);
}
/* Side Drawer */
.comment-drawer {
position: fixed;
top: 0;
right: -400px;
width: 400px;
height: 100%;
background: var(--card-bg);
box-shadow: -2px 0 10px rgba(0,0,0,0.5);
z-index: 1500;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
border-left: 1px solid rgba(255,255,255,0.1);
}
.comment-drawer.visible {
right: 0;
}
后端实现细节
API接口设计
后端定义了段内评论的API接口:
// API Add Inline Comment
svr.Post("/api/inline_comment/add", [&ctrl](const Request &req, Response &resp){
User user;
Json::Value res_json;
if (!ctrl.AuthCheck(req, &user)) {
res_json["status"] = 401;
res_json["reason"] = "请先登录";
resp.set_content(SerializeJson(res_json), "application/json;charset=utf-8");
return;
}
Json::Reader reader;
Json::Value root;
reader.parse(req.body, root);
std::string post_id = root["post_id"].asString();
std::string content = root["content"].asString();
std::string selected_text = root["selected_text"].asString();
std::string parent_id = root.isMember("parent_id") ? root["parent_id"].asString() : "0";
std::string json_out;
ctrl.AddInlineComment(user.id, post_id, content, selected_text, parent_id, &json_out);
resp.set_content(json_out, "application/json;charset=utf-8");
});
// API Delete Inline Comment
svr.Post("/api/inline_comment/delete", [&ctrl](const Request &req, Response &resp){
User user;
Json::Value res_json;
if (!ctrl.AuthCheck(req, &user)) {
res_json["status"] = 401;
res_json["reason"] = "请先登录";
resp.set_content(SerializeJson(res_json), "application/json;charset=utf-8");
return;
}
Json::Reader reader;
Json::Value root;
reader.parse(req.body, root);
std::string comment_id = root["comment_id"].asString();
std::string json_out;
ctrl.DeleteInlineComment(comment_id, user.id, user.role, &json_out);
resp.set_content(json_out, "application/json;charset=utf-8");
});
// API Get Inline Comments
svr.Get(R"(/api/inline_comments/(\d+))", [&ctrl](const Request &req, Response &resp){
std::string post_id = req.matches[1];
std::string json_out;
ctrl.GetInlineComments(post_id, &json_out);
resp.set_content(json_out, "application/json;charset=utf-8");
});
业务逻辑实现
业务逻辑实现:
bool AddInlineComment(const std::string &user_id, const std::string &post_id,
const std::string &content, const std::string &selected_text,
const std::string &parent_id, std::string *json_out)
{
InlineComment c;
c.user_id = user_id;
c.post_id = post_id;
c.content = content;
c.selected_text = selected_text;
c.parent_id = parent_id;
if (model_.AddInlineComment(c)) {
Json::Value res;
res["status"] = 0;
res["reason"] = "Success";
*json_out = SerializeJson(res);
return true;
} else {
Json::Value res;
res["status"] = 1;
res["reason"] = "Database Error";
*json_out = SerializeJson(res);
return false;
}
}
bool GetInlineComments(const std::string &post_id, std::string *json_out)
{
std::vector<InlineComment> comments;
if (model_.GetInlineComments(post_id, &comments)) {
Json::Value root;
root["status"] = 0;
Json::Value list;
for (const auto &c : comments) {
Json::Value item;
item["id"] = c.id;
item["user_id"] = c.user_id;
item["username"] = c.username;
item["avatar"] = c.user_avatar;
item["post_id"] = c.post_id;
item["content"] = c.content;
item["selected_text"] = c.selected_text;
item["parent_id"] = c.parent_id;
item["created_at"] = c.created_at;
list.append(item);
}
root["data"] = list;
*json_out = SerializeJson(root);
return true;
} else {
Json::Value res;
res["status"] = 1;
res["reason"] = "Database Error";
*json_out = SerializeJson(res);
return false;
}
}
数据库设计
表结构
数据模型和表结构定义:
erDiagram
USERS ||--o{ INLINE_COMMENTS : writes
INLINE_COMMENTS ||--o{ INLINE_COMMENTS : replies_to
USERS {
int id PK
varchar username
varchar password
varchar avatar
timestamp created_at
}
INLINE_COMMENTS {
int id PK
int user_id FK
int post_id FK
text content
text selected_text
int parent_id FK
timestamp created_at
}
struct InlineComment
{
std::string id;
std::string user_id;
std::string username; // Join query result
std::string user_avatar; // Join query result
std::string post_id;
std::string content;
std::string selected_text;
std::string parent_id; // For nested replies
std::string created_at;
};
void InitInlineCommentTable() {
std::string sql = "CREATE TABLE IF NOT EXISTS `inline_comments` ("
"`id` int(11) NOT NULL AUTO_INCREMENT,"
"`user_id` int(11) NOT NULL,"
"`post_id` int(11) NOT NULL,"
"`content` TEXT NOT NULL,"
"`selected_text` TEXT,"
"`parent_id` int(11) DEFAULT 0,"
"`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
"PRIMARY KEY (`id`),"
"INDEX `idx_post_id` (`post_id`)"
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
ExecuteSql(sql);
}
数据操作
bool AddInlineComment(const InlineComment &comment)
{
ConnectionGuard guard;
MYSQL *my = guard.get();
if(!my){
return false;
}
auto escape = [&](const std::string &s) -> std::string {
char *buf = new char[s.length() * 2 + 1];
mysql_real_escape_string(my, buf, s.c_str(), s.length());
std::string res(buf);
delete[] buf;
return res;
};
std::string parent_val = comment.parent_id.empty() ? "0" : comment.parent_id;
std::string sql = "INSERT INTO " + oj_inline_comments + " (user_id, post_id, content, selected_text, parent_id) VALUES ('"
+ comment.user_id + "', '"
+ comment.post_id + "', '"
+ escape(comment.content) + "', '"
+ escape(comment.selected_text) + "', "
+ parent_val + ")";
if(0 != mysql_query(my, sql.c_str())) {
LOG(WARNING) << sql << " execute error: " << mysql_error(my) << "\n";
return false;
}
return true;
}
bool GetInlineComments(const std::string &post_id, std::vector<InlineComment> *out)
{
std::string sql = "SELECT c.id, c.user_id, u.username, c.post_id, c.content, c.selected_text, c.created_at, c.parent_id, u.avatar FROM "
+ oj_inline_comments + " c LEFT JOIN " + oj_users + " u ON c.user_id = u.id "
+ "WHERE c.post_id='" + post_id + "' ORDER BY c.created_at ASC";
ConnectionGuard guard;
MYSQL *my = guard.get();
if(!my){
return false;
}
if(0 != mysql_query(my, sql.c_str())) {
return false;
}
MYSQL_RES *res = mysql_store_result(my);
int rows = mysql_num_rows(res);
int fields = mysql_num_fields(res);
for(int i = 0; i < rows; i++)
{
MYSQL_ROW row = mysql_fetch_row(res);
if(row == nullptr) continue;
InlineComment c;
c.id = row[0] ? row[0] : "";
c.user_id = row[1] ? row[1] : "";
c.username = row[2] ? row[2] : "";
c.post_id = row[3] ? row[3] : "";
c.content = row[4] ? row[4] : "";
c.selected_text = row[5] ? row[5] : "";
c.created_at = row[6] ? row[6] : "";
c.parent_id = row[7] ? row[7] : "0";
c.user_avatar = row[8] ? row[8] : "";
out->push_back(c);
}
mysql_free_result(res);
return true;
}
核心功能演示
使用流程
- 选择文本:用户在文章中选中任意文本段落
- 触发评论:系统自动显示评论工具提示
- 输入评论:点击工具提示中的评论按钮,在弹出的模态框中输入评论内容
- 提交评论:点击提交按钮,系统将评论保存到数据库
- 查看评论:评论区域会自动高亮,点击高亮区域可在侧边抽屉中查看所有相关评论
- 回复评论:在侧边抽屉中可以对评论进行回复,支持多层嵌套
sequenceDiagram
participant User as 用户
participant UI as 前端界面
participant API as 后端API
participant DB as 数据库
User->>UI: 选择文本段落
UI->>UI: 显示评论工具提示
User->>UI: 点击评论按钮
UI->>UI: 弹出评论模态框
User->>UI: 输入评论内容
User->>UI: 点击提交按钮
UI->>API: 发送评论数据
API->>DB: 存储评论
DB-->>API: 存储成功
API-->>UI: 返回成功响应
UI->>UI: 关闭模态框
UI->>UI: 高亮显示评论区域
User->>UI: 点击高亮区域
UI->>API: 获取评论列表
API->>DB: 查询评论
DB-->>API: 返回评论数据
API-->>UI: 返回评论列表
UI->>UI: 显示侧边抽屉
User->>UI: 回复评论
UI->>API: 发送回复数据
API->>DB: 存储回复
DB-->>API: 存储成功
API-->>UI: 返回成功响应
UI->>UI: 更新评论列表
交互效果
- 实时高亮:评论的文本段落会自动高亮显示
- 侧边抽屉:点击高亮文本会从右侧滑出评论抽屉,显示所有相关评论
- 嵌套回复:支持评论的多层嵌套,形成讨论线程
- 即时反馈:评论提交后立即更新显示,无需刷新页面
技术亮点与优化
技术亮点
- 精准定位:基于文本选择的评论定位,实现了对具体内容的精准讨论
- 用户体验:流畅的交互体验,包括工具提示、模态框和侧边抽屉
- 数据关联:评论与选中文本的关联存储,确保评论的上下文完整性
- 权限控制:实现了评论的权限管理,只有评论作者和管理员可以删除评论
- 性能优化:使用索引加速查询,减少数据库负载
性能优化
- 数据库索引:在
post_id字段上创建索引,加速评论查询 - 前端缓存:将评论数据缓存到前端,减少重复请求
- 批量操作:一次性加载所有评论,避免多次请求
- 延迟加载:仅在需要时加载评论数据,提高页面加载速度
未来扩展与改进
功能扩展
- 评论点赞:添加评论点赞功能,增加用户互动
- 评论排序:支持按时间、热度等方式排序评论
- 评论通知:当评论被回复时,向用户发送通知
- 评论编辑:允许用户编辑自己的评论
- 评论举报:添加评论举报功能,维护社区秩序
技术改进
- 前端框架迁移:将前端从原生JS迁移到React,提高代码可维护性
- WebSocket集成:使用WebSocket实现评论的实时更新
- 缓存优化:集成Redis缓存,进一步提高性能
- 搜索功能:添加评论搜索功能,方便用户查找相关讨论
- 响应式设计:优化移动端体验,确保在各种设备上的良好表现
总结
段内评论功能是一种重要的技术创新,它为用户提供了一种全新的互动方式,使讨论更加精准和深入。通过前后端的紧密配合,实现了从文本选择到评论展示的完整流程,为在线内容的讨论和协作提供了有力支持。
该功能的实现充分利用了现代Web技术,包括原生JS的选择API、CSS的过渡动画、后端的RESTful API以及数据库的关联查询。同时,通过合理的架构设计和性能优化,确保了在高并发场景下的稳定运行。
未来,段内评论技术将继续发展,添加更多交互特性,提高用户体验,成为各类在线平台的核心功能之一。