import { CodeArea } from '../CodeArea.ts';
import { util } from '../util.ts';

/**
 * 按钮状态类型
 */
type ButtonState = 'normal' | 'loading' | 'error';

/**
 * 抽屉上下文数据接口
 */
interface DrawerContext {
    avatarSrc: string;
    authorName: string;
    contentText: string;
}

/**
 * 图标集合接口
 */
interface IconCollection {
    loading: string;
    send: string;
    error: string;
}

/**
 * 声明全局 LazyLoad
 */
declare const LazyLoad: any;

/**
 * 评论 UI 模块
 * 负责 DOM 操作和交互处理
 */
export class CommentUI {
    container: HTMLElement | null;
    listContainer: HTMLElement | null;
    drawer: HTMLElement | null;
    drawerOverlay: HTMLElement | null;
    icons: IconCollection;

    /**
     * 构造函数
     * @param element 评论容器元素,默认查找 .comment-section
     */
    constructor(element?: HTMLElement | null) {
        this.container = element || document.querySelector<HTMLElement>('.comment-section');
        this.listContainer = this.container
            ? this.container.querySelector<HTMLElement>('.comment-list')
            : null;
        this.drawer = null;
        this.drawerOverlay = null;

        this.icons = {
            loading: `<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"></path></svg>`,
            send: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22,2 15,22 11,13 2,9 22,2"></polygon></svg>`,
            error: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`,
        };
    }

    /**
     * 更新提交按钮状态
     * @param btn 按钮元素
     * @param state 按钮状态:normal-正常、loading-加载中、error-错误
     * @param msg 自定义消息文本
     */
    public setButtonState(btn: HTMLButtonElement | null, state: ButtonState, msg?: string): void {
        if (!btn) {
            return;
        }

        switch (state) {
            case 'loading':
                btn.disabled = true;
                btn.innerHTML = `${this.icons.loading} 发送中...`;
                btn.classList.add('btn-loading');
                break;
            case 'error':
                btn.disabled = false;
                btn.innerHTML = `${this.icons.error} ${msg || '重试'}`;
                btn.classList.add('btn-error');
                break;
            case 'normal':
            default:
                btn.disabled = false;
                btn.innerHTML = `${this.icons.send} 发送`;
                btn.classList.remove('btn-loading', 'btn-error');
                break;
        }
    }

    /**
     * 显示骨架屏加载状态
     * 替换整个容器内容为加载动画
     */
    public showListSkeleton(): void {
        if (!this.container) {
            return;
        }

        const skeletonHtml = `
            <div class="comment-section-skeleton">
                <div class="comment-loading-spinner">
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin">
                        <path d="M21 12a9 9 0 11-6.219-8.56"></path>
                    </svg>
                    <span>评论加载中...</span>
                </div>
            </div>
        `;

        const wrapper = this.container.parentElement;
        if (wrapper) {
            wrapper.innerHTML = skeletonHtml;
        }
    }

    /**
     * 显示评论列表加载状态
     * 在列表上方显示遮罩层和加载动画
     */
    public showListLoading(): void {
        if (!this.listContainer) {
            return;
        }

        let overlay = this.listContainer.querySelector<HTMLElement>('.comment-list-loading');
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.className = 'comment-list-loading';
            overlay.innerHTML = `
                <div class="comment-loading-spinner">
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin">
                        <path d="M21 12a9 9 0 11-6.219-8.56"></path>
                    </svg>
                    <span>加载中...</span>
                </div>
            `;
            if (getComputedStyle(this.listContainer).position === 'static') {
                this.listContainer.style.position = 'relative';
            }
            this.listContainer.appendChild(overlay);
        }

        overlay.style.display = 'flex';
        this.listContainer.classList.add('is-loading');
    }

    /**
     * 隐藏评论列表加载状态
     */
    public hideListLoading(): void {
        if (!this.listContainer) {
            return;
        }

        const overlay = this.listContainer.querySelector<HTMLElement>('.comment-list-loading');
        if (overlay) {
            overlay.style.display = 'none';
        }
        this.listContainer.classList.remove('is-loading');
    }

    /**
     * 替换评论列表内容
     * @param html 新的HTML内容
     */
    updateList(html: string): void {
        if (this.listContainer) {
            this.listContainer.innerHTML = html;
            this.refreshPlugins();
            this.listContainer.classList.remove('is-loading');
        }
    }

    /**
     * 追加评论到列表末尾
     * 用于加载更多评论的场景
     * @param html 要追加的HTML内容
     */
    appendList(html: string): void {
        if (this.listContainer) {
            const oldLoadMore = this.listContainer.querySelector('.comment-load-more');
            if (oldLoadMore) {
                oldLoadMore.remove();
            }

            const temp = document.createElement('div');
            temp.innerHTML = html;

            while (temp.firstChild) {
                this.listContainer.appendChild(temp.firstChild);
            }

            this.refreshPlugins();
        }
    }

    /**
     * 更新评论数量显示
     * @param count 评论数量字符串
     */
    updateCount(count: string): void {
        const countEl = this.container?.querySelector('.comment-section__count');
        if (countEl && count) {
            countEl.textContent = count;
        }
    }

    /**
     * 刷新插件
     * 重新初始化代码高亮、时间标签、图片懒加载等插件
     */
    refreshPlugins(): void {
        if (this.container && typeof CodeArea !== 'undefined') {
            new CodeArea(this.container);
        }

        if (this.container) {
            util.updateTimeTags(this.container);
        }

        if (this.container && typeof LazyLoad !== 'undefined') {
            new LazyLoad({
                container: this.container,
            });
        }
    }

    /**
     * 显示回复表单
     * 根据设备类型选择桌面端内联显示或移动端抽屉显示
     * @param commentItem 要回复的评论元素
     * @param replyId 回复目标的评论ID
     */
    showReplyForm(commentItem: HTMLElement, replyId: string): void {
        let form = this.container?.querySelector<HTMLFormElement>('form.comment-form') || null;

        if (!form && this.drawer) {
            form = this.drawer.querySelector<HTMLFormElement>('form.comment-form');
            if (form) {
                const originalWrapper = this.container?.querySelector('.comment-form-wrapper');
                if (originalWrapper) {
                    originalWrapper.appendChild(form);
                    form.classList.remove('comment-form--reply');
                }
            }
        }

        if (!form) {
            return;
        }

        const isMobile = window.innerWidth <= 768;

        if (isMobile) {
            this.showMobileReplyDrawer(commentItem, replyId, form);
        } else {
            this.showDesktopReplyForm(commentItem, replyId, form);
        }
    }

    /**
     * 桌面端回复表单显示
     * 将表单内联插入到目标评论下方
     * @param commentItem 要回复的评论元素
     * @param replyId 回复目标的评论ID
     * @param form 表单元素
     */
    private showDesktopReplyForm(
        commentItem: HTMLElement,
        replyId: string,
        form: HTMLFormElement
    ): void {
        this.resetForm(form);

        const replyInput = form.querySelector<HTMLInputElement>('input[name="replyId"]');
        if (replyInput) {
            replyInput.value = replyId;
        }

        const cancelBtn = form.querySelector<HTMLElement>('.comment-form__cancel-btn');
        if (cancelBtn) {
            cancelBtn.style.display = 'inline-block';
        }

        const targetBody = commentItem.querySelector('.comment-item__body');
        const actionsEl = targetBody?.querySelector('.comment-item__actions');

        if (actionsEl) {
            actionsEl.after(form);
        } else if (targetBody) {
            targetBody.appendChild(form);
        }

        form.classList.add('comment-form--reply');

        form.scrollIntoView({ behavior: 'smooth', block: 'center' });

        const textarea = form.querySelector<HTMLTextAreaElement>('textarea');
        if (textarea) {
            textarea.focus();
        }
    }

    /**
     * 移动端回复表单抽屉显示
     * 在底部抽屉中显示回复表单和上下文信息
     * @param commentItem 要回复的评论元素
     * @param replyId 回复目标的评论ID
     * @param form 表单元素
     */
    private showMobileReplyDrawer(
        commentItem: HTMLElement,
        replyId: string,
        form: HTMLFormElement
    ): void {
        const avatar = commentItem.querySelector<HTMLImageElement>('.comment-avatar-img');
        const author = commentItem.querySelector('.comment-item__author, .comment-item__user-info');
        const content = commentItem.querySelector('.comment-item__content');

        const contextData: DrawerContext = {
            avatarSrc: avatar ? avatar.src : '',
            authorName: author ? author.textContent?.trim() || '未知用户' : '未知用户',
            contentText: content ? content.textContent?.trim().substring(0, 100) || '' : '',
        };

        if (!this.drawer) {
            this.createMobileDrawer();
        }

        this.updateDrawerContext(contextData);

        this.resetForm(form);
        const replyInput = form.querySelector<HTMLInputElement>('input[name="replyId"]');
        if (replyInput) {
            replyInput.value = replyId;
        }

        const drawerBody = this.drawer?.querySelector('.comment-reply-drawer__body');
        if (drawerBody) {
            drawerBody.innerHTML = '';
            drawerBody.appendChild(form);
        }

        this.openDrawer();
    }

    /**
     * 创建移动端抽屉结构
     */
    private createMobileDrawer(): void {
        this.drawerOverlay = document.createElement('div');
        this.drawerOverlay.className = 'comment-reply-drawer__overlay';
        this.drawerOverlay.addEventListener('click', () => this.closeDrawer());

        this.drawer = document.createElement('div');
        this.drawer.className = 'comment-reply-drawer';
        this.drawer.innerHTML = `
            <div class="comment-reply-drawer__header">
                <span class="comment-reply-drawer__title">回复评论</span>
                <button type="button" class="comment-reply-drawer__close" aria-label="关闭">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            </div>
            <div class="comment-reply-drawer__context">
                <div class="comment-reply-drawer__context-label">回复给</div>
                <div class="comment-reply-drawer__context-item">
                    <img class="comment-reply-drawer__context-avatar" src="" alt="">
                    <div class="comment-reply-drawer__context-body">
                        <div class="comment-reply-drawer__context-author"></div>
                        <div class="comment-reply-drawer__context-text"></div>
                    </div>
                </div>
            </div>
            <div class="comment-reply-drawer__body"></div>
        `;

        const closeBtn = this.drawer.querySelector('.comment-reply-drawer__close');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => this.closeDrawer());
        }

        this.container?.appendChild(this.drawerOverlay);
        this.container?.appendChild(this.drawer);
    }

    /**
     * 更新抽屉上下文信息
     * @param data 上下文数据(头像、作者、内容)
     */
    private updateDrawerContext(data: DrawerContext): void {
        if (!this.drawer) {
            return;
        }

        const avatar = this.drawer.querySelector<HTMLImageElement>(
            '.comment-reply-drawer__context-avatar'
        );
        const author = this.drawer.querySelector('.comment-reply-drawer__context-author');
        const text = this.drawer.querySelector('.comment-reply-drawer__context-text');

        if (avatar) {
            avatar.src = data.avatarSrc;
        }
        if (author) {
            author.textContent = data.authorName;
        }
        if (text) {
            text.textContent = data.contentText;
        }
    }

    /**
     * 打开抽屉
     */
    private openDrawer(): void {
        if (!this.drawer || !this.drawerOverlay) {
            return;
        }

        document.body.style.overflow = 'hidden';

        this.drawerOverlay.classList.add('comment-reply-drawer__overlay--visible');
        requestAnimationFrame(() => {
            this.drawer?.classList.add('comment-reply-drawer--open');
        });

        setTimeout(() => {
            const textarea =
                this.drawer?.querySelector<HTMLTextAreaElement>('.comment-form__editor');
            if (textarea) {
                textarea.focus();
            }
        }, 350);
    }

    /**
     * 关闭抽屉
     */
    closeDrawer(): void {
        if (!this.drawer || !this.drawerOverlay) {
            return;
        }

        const form = this.drawer.querySelector<HTMLFormElement>('form.comment-form');
        if (form) {
            const originalWrapper = this.container?.querySelector('.comment-form-wrapper');
            if (originalWrapper) {
                this.resetForm(form);
                originalWrapper.appendChild(form);
                form.classList.remove('comment-form--reply');

                const cancelBtn = form.querySelector<HTMLElement>('.comment-form__cancel-btn');
                if (cancelBtn) {
                    cancelBtn.style.display = 'none';
                }
            }
        }

        this.drawer.classList.remove('comment-reply-drawer--open');
        this.drawerOverlay.classList.remove('comment-reply-drawer__overlay--visible');

        document.body.style.overflow = '';
    }

    /**
     * 重置回复表单到原始位置
     */
    resetReplyForm(): void {
        const form =
            this.container?.querySelector<HTMLFormElement>('form.comment-form') ||
            this.drawer?.querySelector<HTMLFormElement>('form.comment-form') ||
            null;
        const originalWrapper = this.container?.querySelector('.comment-form-wrapper');

        if (form && originalWrapper) {
            this.resetForm(form);
            originalWrapper.appendChild(form);
            form.classList.remove('comment-form--reply');

            const cancelBtn = form.querySelector<HTMLElement>('.comment-form__cancel-btn');
            if (cancelBtn) {
                cancelBtn.style.display = 'none';
            }
        }

        if (this.drawer && this.drawer.classList.contains('comment-reply-drawer--open')) {
            this.drawer.classList.remove('comment-reply-drawer--open');
            if (this.drawerOverlay) {
                this.drawerOverlay.classList.remove('comment-reply-drawer__overlay--visible');
            }
            document.body.style.overflow = '';
        }
    }

    /**
     * 重置表单状态
     * @param form 表单元素
     */
    public resetForm(form: HTMLFormElement): void {
        const profileInputs = {
            nickname: form.querySelector<HTMLInputElement>('input[name="nickname"]')?.value ?? '',
            email: form.querySelector<HTMLInputElement>('input[name="email"]')?.value ?? '',
            site: form.querySelector<HTMLInputElement>('input[name="site"]')?.value ?? '',
        };

        form.reset();

        const nicknameInput = form.querySelector<HTMLInputElement>('input[name="nickname"]');
        if (nicknameInput) {
            nicknameInput.value = profileInputs.nickname;
        }

        const emailInput = form.querySelector<HTMLInputElement>('input[name="email"]');
        if (emailInput) {
            emailInput.value = profileInputs.email;
        }

        const siteInput = form.querySelector<HTMLInputElement>('input[name="site"]');
        if (siteInput) {
            siteInput.value = profileInputs.site;
        }

        const replyInput = form.querySelector<HTMLInputElement>('input[name="replyId"]');
        if (replyInput) {
            replyInput.value = '';
        }
        this.setButtonState(
            form.querySelector<HTMLButtonElement>('button[type="submit"]'),
            'normal'
        );
        this.hideListLoading();
        const textarea = form.querySelector<HTMLTextAreaElement>('.comment-form__editor');
        if (textarea) {
            this.updateCharCount(textarea);
        }
    }

    /**
     * 插入 Markdown 文本到编辑器
     * @param text 要插入的Markdown文本
     */
    public insertMarkdown(text: string): void {
        const form = this.container?.querySelector<HTMLFormElement>('form.comment-form');
        const textarea = form?.querySelector<HTMLTextAreaElement>('textarea') || null;
        if (textarea) {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const value = textarea.value;
            textarea.value = value.substring(0, start) + text + value.substring(end);
            textarea.focus();
            textarea.selectionStart = textarea.selectionEnd = start + text.length;
            this.updateCharCount(textarea);
        }
    }

    /**
     * 更新字符计数显示
     * @param textarea 文本域元素
     */
    public updateCharCount(textarea: HTMLTextAreaElement): void {
        const form = textarea.closest<HTMLFormElement>('.comment-form');
        if (!form) {
            return;
        }

        const counter = form.querySelector('.comment-form__char-count');
        if (!counter) {
            return;
        }

        const max = parseInt(textarea.getAttribute('maxlength') || '1000');
        const current = textarea.value.length;
        counter.textContent = `${current}/${max}`;

        counter.classList.remove(
            'comment-form__char-count--warn',
            'comment-form__char-count--limit'
        );
        if (current >= max) {
            counter.classList.add('comment-form__char-count--limit');
        } else if (current > max * 0.9) {
            counter.classList.add('comment-form__char-count--warn');
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个功能完整的评论系统UI控制类,主要负责处理评论界面的DOM操作和用户交互。让我详细解释一下代码的功能:

类的主要职责

CommentUI 类是一个评论系统的UI层控制器,专门处理界面展示和用户交互逻辑。

核心功能模块

1. 初始化和配置

constructor(element?: HTMLElement | null)
  • 初始化容器元素(默认查找 .comment-section
  • 配置SVG图标集合(加载中、发送、错误状态图标)
  • 设置相关DOM元素引用

2. 按钮状态管理

setButtonState(btn: HTMLButtonElement | null, state: ButtonState, msg?: string)
  • 控制提交按钮的三种状态:正常、加载中、错误
  • 动态更换图标和文字
  • 管理按钮的可用/禁用状态

3. 加载状态展示

  • showListSkeleton(): 显示骨架屏加载动画
  • showListLoading(): 在列表上显示加载遮罩
  • hideListLoading(): 隐藏加载状态

4. 列表内容管理

  • updateList(html): 完全替换评论列表内容
  • appendList(html): 追加新评论(用于分页加载)
  • updateCount(count): 更新评论数量显示

5. 回复功能核心特性

响应式设计

  • 桌面端: 内联显示回复表单在目标评论下方
  • 移动端: 底部抽屉弹窗显示回复界面

桌面端回复 (showDesktopReplyForm)

private showDesktopReplyForm(commentItem, replyId, form)
  • 将表单插入到被回复评论的下方
  • 设置回复目标ID
  • 平滑滚动到表单位置并自动聚焦

移动端回复 (showMobileReplyDrawer)

private showMobileReplyDrawer(commentItem, replyId, form)
  • 提取被回复评论的上下文信息(头像、作者、内容预览)
  • 在底部抽屉中显示完整的回复界面
  • 提供更好的移动端用户体验

6. 抽屉组件管理

  • createMobileDrawer(): 动态创建抽屉DOM结构
  • openDrawer() / closeDrawer(): 控制抽屉的显示/隐藏
  • updateDrawerContext(): 更新抽屉中的上下文信息

7. 表单管理

  • resetForm(): 重置表单状态,保留用户个人信息
  • resetReplyForm(): 将回复表单恢复到原始位置
  • insertMarkdown(): 向编辑器插入Markdown文本
  • updateCharCount(): 实时更新字符计数显示

8. 插件集成

refreshPlugins()
  • 重新初始化代码高亮 (CodeArea)
  • 更新时间标签显示 (util.updateTimeTags)
  • 重新激活图片懒加载 (LazyLoad)

设计亮点

1. 响应式交互设计

根据屏幕宽度自动选择最适合的回复界面展示方式:

  • 桌面端:内联表单,上下文更清晰
  • 移动端:抽屉弹窗,空间利用更充分

2. 状态管理完善

  • 按钮状态可视化反馈
  • 加载状态的多种展示形式
  • 表单状态的智能重置

3. 用户体验优化

  • 平滑滚动和自动聚焦
  • 字符计数和限制提醒
  • 上下文信息保持(回复时显示原评论信息)

4. 模块化架构

  • 职责单一,专注UI控制
  • 与业务逻辑解耦
  • 易于测试和维护

这个类为评论系统提供了完整的UI交互控制层,特别在移动端适配和用户体验方面做了很多细致的优化。

评论加载中...