/**
 * 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 = `![](${url.toString()})`;
        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(),以便未来解绑。

提交评论(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 需要补全以避免重复绑定的问题。
评论加载中...