/**
* Comment System Entry Point
*/
import { CommentApi } from './api.js';
import { CommentUI } from './ui.js';
import { dialog } from '../dialog.js';
export class CommentSystem {
constructor(element) {
this.api = new CommentApi();
this.ui = new CommentUI(element);
this.init();
}
init() {
if (!this.ui.container) return;
console.log('Comment System Initialized');
// Unbind previous events to avoid duplicates if re-initialized
this.unbindEvents();
this.bindEvents();
this.ui.refreshPlugins();
}
unbindEvents() {
if (this.ui.container.commentSystem) {
}
this.ui.container.commentSystem = this;
}
bindEvents() {
const container = this.ui.container;
if (!container) return;
const formHandler = async (e) => {
if (e.target.matches('form.comment-form')) {
e.preventDefault();
e.stopPropagation();
await this.handleSubmit(e.target);
}
};
container.addEventListener('submit', formHandler);
const clickHandler = async (e) => {
// Reply Button
const replyBtn = e.target.closest('.comment-item__reply-btn');
if (replyBtn) {
const commentItem = replyBtn.closest('.comment-item');
const replyId = replyBtn.dataset.replyId;
this.ui.showReplyForm(commentItem, replyId);
return;
}
// Cancel Reply
if (e.target.matches('.comment-form__cancel-btn')) {
this.ui.resetReplyForm();
// Also close drawer if open
if (this.ui.drawer && this.ui.drawer.classList.contains('comment-reply-drawer--open')) {
this.ui.closeDrawer();
}
return;
}
// Refresh Button
const refreshBtn = e.target.closest('.comment-section__refresh-btn');
if (refreshBtn) {
const url = refreshBtn.dataset.url;
await this.handleRefresh(url, refreshBtn);
return;
}
// Load More
const loadMoreBtn = e.target.closest('.comment-load-more__btn');
if (loadMoreBtn) {
const url = loadMoreBtn.dataset.pageRequest;
const pageIndex = parseInt(loadMoreBtn.dataset.pageIndex);
const pageSize = parseInt(loadMoreBtn.dataset.pageSize);
const separator = url.includes('?') ? '&' : '?';
const fullUrl = `${url}${separator}pageIndex=${pageIndex + 1}&pageSize=${pageSize}`;
await this.handleLoadMore(fullUrl, loadMoreBtn);
return;
}
// Image Insert
const imgBtn = e.target.closest('[data-action="image"]');
if (imgBtn) {
await this.handleImageInsert();
//return;
}
};
container.addEventListener('click', clickHandler);
// Keydown
const keydownHandler = (e) => {
if (e.target.matches('form.comment-form input') && e.key === 'Enter') {
e.preventDefault();
}
};
container.addEventListener('keydown', keydownHandler);
// Character counter
const inputHandler = (e) => {
if (e.target.matches('.comment-form__editor')) {
this.ui.updateCharCount(e.target);
}
};
container.addEventListener('input', inputHandler);
// Store cleanup function
this.cleanup = () => {
container.removeEventListener('submit', formHandler);
container.removeEventListener('click', clickHandler);
container.removeEventListener('keydown', keydownHandler);
container.removeEventListener('input', inputHandler);
};
}
async handleSubmit(form) {
const submitBtn = form.querySelector('button[type="submit"]');
this.ui.setButtonState(submitBtn, 'loading');
const formData = new FormData(form);
const url = form.action;
const result = await this.api.postComment(url, formData);
if (result.success) {
this.ui.resetReplyForm();
if (result.html) {
this.ui.updateList(result.html);
}
if (result.commentCount) {
this.ui.updateCount(result.commentCount);
}
this.ui.resetForm(form);
// Close drawer if open
if (this.ui.drawer && this.ui.drawer.classList.contains('comment-reply-drawer--open')) {
this.ui.closeDrawer();
}
dialog.toast('评论发布成功');
} else {
this.ui.setButtonState(submitBtn, 'error', '重试');
dialog.toast(result.msg || '发布失败', 'error');
setTimeout(() => {
this.ui.setButtonState(submitBtn, 'normal');
}, 3000);
}
}
async handleRefresh(url, btn) {
// Prevent multiple clicks
if (btn.disabled || btn.classList.contains('is-loading')) return;
btn.classList.add('fa-spin');
btn.classList.add('is-loading');
// Capture wrapper BEFORE showListSkeleton detaches the container
// this.ui.container is the .comment-section element
const wrapper = this.ui.container.parentElement;
this.ui.showListSkeleton();
const separator = url.includes('?') ? '&' : '?';
const refreshUrl = `${url}${separator}t=${new Date().getTime()}`;
this.ui.resetReplyForm();
const result = await this.api.loadComments(refreshUrl);
if (result.success) {
if (wrapper) {
wrapper.innerHTML = result.html;
// Re-init system for new DOM
const newContainer = wrapper.querySelector('.comment-section');
new CommentSystem(newContainer);
}
dialog.toast('刷新成功');
} else {
dialog.toast('刷新失败', 'error');
window.location.reload();
}
}
async handleLoadMore(url, btn) {
const originalText = btn.innerHTML;
btn.innerHTML = '加载中...';
btn.disabled = true;
const result = await this.api.loadComments(url);
if (result.success) {
if (result.html) {
this.ui.appendList(result.html);
}
if (result.commentCount) {
this.ui.updateCount(result.commentCount);
}
} else {
btn.innerHTML = '加载失败,点击重试';
btn.disabled = false;
}
}
async handleImageInsert() {
const value = await dialog.prompt('请输入网络图片地址', '请输入网络图片地址', 'https://');
if (value === null) { return; }
let url;
try {
url = new URL(value);
} catch {
dialog.toast('请输入正确的地址');
return;
}
if (url.protocol !== "https:") {
dialog.toast('请使用HTTPS协议的图片地址');
return;
}
const markdownImg = `})`;
this.ui.insertMarkdown(markdownImg);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面按模块与流程来解释这段代码的功能、行为和关键点(中文说明):
总体概述
- 这是一个评论系统的入口类 CommentSystem,用来把后端 API(CommentApi)和前端 UI(CommentUI)以及对话/提示(dialog)结合起来,负责事件绑定、表单提交、翻页/刷新、图片插入等交互逻辑。
- 在构造函数中,它创建 api 与 ui 实例,并调用 init() 完成初始化。
构造与初始化(constructor / init)
- constructor(element):
- this.api = new CommentApi():封装与后端交互的方法(如发布评论、加载评论等)。
- this.ui = new CommentUI(element):封装 DOM 操作(更新列表、显示回复表单、字符计数、骨架屏等)。
- this.init():初始化交互逻辑。
- init():
- 如果 this.ui.container 不存在则直接返回(说明页面上没有评论区)。
- 打印初始化日志,调用 unbindEvents()(意图是解绑之前绑定的事件以免重复绑定),再调用 bindEvents() 重新绑定事件,并调用 this.ui.refreshPlugins()(可能是初始化富文本编辑器或插件)。
事件解绑(unbindEvents)
- 当前实现有问题/不完整:函数体没有实际解绑已绑定的事件,唯一做的是设置 this.ui.container.commentSystem = this;(应该是想保存实例或标记)。正常情况下应该检查并调用之前保存的 cleanup 或移除事件监听器。
事件绑定(bindEvents)
- 先检查 container 是否存在。然后在 container 上注册多个事件监听器,并将一个 cleanup 函数保存在 this.cleanup,供将来移除这些监听器:
- submit 事件(formHandler):
- 监测目标是否是 .comment-form 表单,阻止默认提交并调用 this.handleSubmit(form) 以异步提交。
- click 事件(clickHandler):处理多种点击交互:
- 回复按钮(.comment-item__reply-btn):定位到对应的评论项并调用 this.ui.showReplyForm(commentItem, replyId) 显示回复表单。
- 取消回复(.comment-form__cancel-btn):重置回复表单,并在抽屉(drawer)打开时关闭它。
- 刷新按钮(.comment-section__refresh-btn):读取 data-url 并调用 this.handleRefresh(url, btn) 刷新评论列表。
- 加载更多(.comment-load-more__btn):读取分页参数,构造下一页请求 URL 并调用 handleLoadMore。
- 图片插入([data-action="image"]):调用 handleImageInsert 弹窗输入图片地址并插入。
- keydown 事件(keydownHandler):
- 当焦点位于 form.comment-form input 且按下 Enter 时阻止默认提交(避免回车触发表单提交)。
- input 事件(inputHandler):
- 当输入域匹配 .comment-form__editor 时,调用 this.ui.updateCharCount 更新字符计数器。
- 最后把对应的 removeEventListener 操作封装到 this.cleanup(),以便未来解绑。
- submit 事件(formHandler):
提交评论(handleSubmit)
- 接收表单节点 form:
- 将提交按钮设置为 loading 状态(this.ui.setButtonState)。
- 使用 FormData 获取表单数据并通过 this.api.postComment(url, formData) 发送到后端。
- 如果 result.success 为真:
- 重置回复表单(this.ui.resetReplyForm)。
- 如果返回了 html,则通过 this.ui.updateList(result.html) 更新评论列表(可能是替换列表)。
- 如果返回了 commentCount,则更新显示的评论数量。
- 重置表单(清空输入等)。
- 关闭抽屉(如果打开)。
- 使用 dialog.toast 显示成功提示。
- 如果失败:
- 把按钮状态设为 error 并显示“重试”文本,显示错误 toast,3 秒后把按钮回复到 normal。
刷新评论(handleRefresh)
- 防抖小措施:如果按钮正在禁用或带 is-loading 则直接返回,避免重复刷新。
- 给按钮加样式(fa-spin, is-loading)。
- 在调用 this.ui.showListSkeleton()(显示加载骨架,可能导致原 container 被替换或从 DOM 中分离)前,先保存 wrapper = this.ui.container.parentElement(保存最外层包裹节点),以便后续把返回的 html 放回去。
- 构造带时间戳的 URL(避免缓存),重置回复表单,调用 this.api.loadComments(refreshUrl)。
- 如果成功:
- 把 wrapper.innerHTML 替换为返回的 result.html。
- 在新的 DOM 中找到新的 .comment-section 并 new CommentSystem(newContainer) 重新初始化评论系统(保证新的 DOM 上事件会重新绑定)。
- 显示“刷新成功”提示。
- 如果失败:显示失败提示并刷新当前页面(window.location.reload())。
加载更多(handleLoadMore)
- 将按钮文本置为“加载中...”,禁用按钮。
- 调用 this.api.loadComments(url) 获取下一页。
- 成功时:
- 如果返回 html,则通过 this.ui.appendList(result.html) 将新评论插入到列表尾部。
- 如果有 commentCount,更新总数。
- 失败时:将按钮文本改为“加载失败,点击重试”并恢复可点击状态。
插入网络图片(handleImageInsert)
- 用 dialog.prompt 弹窗询问用户图片地址,默认值 https://。
- 如果用户取消(value === null)则返回。
- 使用 URL 构造器验证地址是否为合法 URL;如果解析失败则提示“请输入正确的地址”。
- 强制要求 https 协议(url.protocol === "https:"),否则提示要求 HTTPS。
- 合法后构造 Markdown 图片语法
并通过 this.ui.insertMarkdown 插入到当前编辑器中。
依赖与假设
- 依赖三个模块:
- CommentApi:负责与后端交互的方法,如 postComment、loadComments。
- CommentUI:负责 DOM 操作,如 updateList、appendList、showReplyForm、resetForm、showListSkeleton、insertMarkdown、updateCharCount 等。
- dialog:用于弹窗与轻提示(toast、prompt)。
- 代码假设 DOM 结构中存在若干带特定类名和 data- 属性的元素(如 .comment-section、.comment-item、.comment-form、.comment-load-more__btn 等)。
可能的问题/改进建议(可选)
- unbindEvents 实现不完整:应该检查并调用 this.cleanup() 来移除先前绑定的事件,当前只把实例写入 container.commentSystem,不能真正解绑事件,会造成重复绑定问题。
- handleRefresh 在直接替换 wrapper.innerHTML 并 new CommentSystem 时需要确保所有外部状态(例如全局插件)能被正确重新初始化。
- 错误处理可以更细化(区分网络错误与验证错误)。
总结
- 这个类是评论系统的前端控制器,连接 UI 与 API,管理事件绑定、表单提交、刷新、分页、图片插入和用户提示等交互逻辑。注意当前 unbindEvents 需要补全以避免重复绑定的问题。
评论加载中...