/** * CommentItem 评论项组件 */ import { Component } from './Component.js'; import { ReplyEditor } from './ReplyEditor.js'; import { formatRelativeTime } from '@/utils/date.js'; export class CommentItem extends Component { /** * @param {HTMLElement|string} container - 容器元素或选择器 * @param {Object} props - 组件属性 * @param {Object} props.comment - 评论数据 * @param {boolean} props.isReply - 是否为回复 * @param {number|null} props.replyingTo - 当前正在回复的评论 ID * @param {string} props.replyContent - 回复内容 * @param {string|null} props.replyError - 回复错误 * @param {boolean} props.submitting - 是否正在提交 * @param {string} props.adminEmail - 博主邮箱(可选) * @param {string} props.adminBadge - 博主标识文字(可选) * @param {Function} props.onReply - 回复回调 * @param {Function} props.onSubmitReply - 提交回复回调 * @param {Function} props.onCancelReply - 取消回复回调 * @param {Function} props.onUpdateReplyContent - 更新回复内容回调 * @param {Function} props.onClearReplyError - 清除回复错误回调 */ constructor(container, props = {}) { super(container, props); this.replyEditor = null; this.childCommentItems = []; // 缓存嵌套回复的 CommentItem 实例 } render() { const { comment, isReply, adminEmail, adminBadge } = this.props; const isReplying = this.props.replyingTo === comment.id; const isAdmin = adminEmail && comment.email === adminEmail; const root = this.createElement('div', { className: `cwd-comment-item ${isReply ? 'cwd-comment-reply' : ''}`, children: [ // 头像 this.createElement('div', { className: 'cwd-comment-avatar', children: [ this.createElement('img', { attributes: { src: comment.avatar, alt: comment.author, loading: 'lazy' } }) ] }), // 主体内容 this.createElement('div', { className: 'cwd-comment-body', children: [ // 头部(作者名、操作按钮、时间) this.createElement('div', { className: 'cwd-comment-header', children: [ // 作者信息 this.createElement('div', { className: 'cwd-comment-author', children: [ comment.url ? this.createElement('span', { className: 'cwd-author-name', children: [ this.createElement('a', { attributes: { href: comment.url, target: '_blank', rel: 'noopener noreferrer' }, text: comment.author }) ] }) : this.createTextElement('span', comment.author, 'cwd-author-name'), // 博主标识 ...(isAdmin ? [ this.createTextElement('span', `${adminBadge}`, 'cwd-admin-badge') ] : []), // 显示回复目标 ...(comment.replyToAuthor ? [ this.createTextElement('span', ' 回复 ', 'cwd-reply-to-separator'), this.createTextElement('span', comment.replyToAuthor, 'cwd-reply-to-author') ] : []) ] }), // 操作区域 this.createElement('div', { className: 'cwd-comment-actions', children: [ this.createElement('span', { className: 'cwd-action-btn', attributes: { onClick: () => this.handleReply() }, text: '回复' }), this.createTextElement('span', formatRelativeTime(comment.pubDate), 'cwd-comment-time') ] }) ] }), // 评论内容 this.createElement('div', { className: 'cwd-comment-content' }), // 回复编辑器容器 this.createElement('div', { className: 'cwd-reply-editor-container' }), // 嵌套回复容器 ...(comment.replies && comment.replies.length > 0 ? [ this.createElement('div', { className: 'cwd-replies' }) ] : []) ] }) ] }); // 设置评论内容的 HTML const contentEl = root.querySelector('.cwd-comment-content'); if (contentEl) { contentEl.innerHTML = comment.contentHtml; } // 创建回复编辑器 if (isReplying) { const replyContainer = root.querySelector('.cwd-reply-editor-container'); if (replyContainer) { this.replyEditor = new ReplyEditor(replyContainer, { replyToAuthor: comment.author, content: this.props.replyContent, error: this.props.replyError, submitting: this.props.submitting, onUpdate: (content) => this.handleUpdateReplyContent(content), onSubmit: () => this.handleSubmitReply(), onCancel: () => this.handleCancelReply(), onClearError: () => this.handleClearReplyError() }); this.replyEditor.render(); this.replyEditor.focus(); } } else { this.replyEditor = null; } // 渲染嵌套回复 this.childCommentItems = []; if (comment.replies && comment.replies.length > 0) { const repliesContainer = root.querySelector('.cwd-replies'); if (repliesContainer) { comment.replies.forEach(reply => { const replyItem = new CommentItem(repliesContainer, { comment: reply, isReply: true, replyingTo: this.props.replyingTo, replyContent: this.props.replyContent, replyError: this.props.replyError, submitting: this.props.submitting, adminEmail: this.props.adminEmail, adminBadge: this.props.adminBadge, onReply: this.props.onReply, onSubmitReply: this.props.onSubmitReply, onCancelReply: this.props.onCancelReply, onUpdateReplyContent: this.props.onUpdateReplyContent, onClearReplyError: this.props.onClearReplyError }); replyItem.render(); this.childCommentItems.push(replyItem); }); } } this.elements.root = root; // 只在首次渲染时清空容器(当还没有 root 元素时) if (this.container.contains(root)) { // 如果 root 已存在,替换它 this.container.replaceChild(root, this.elements.root); } else { // 否则直接添加 this.container.appendChild(root); } } updateProps(prevProps) { const { comment } = this.props; const wasReplying = prevProps.replyingTo === comment.id; const isReplying = this.props.replyingTo === comment.id; // 如果评论数据本身变化,需要完全重新渲染 if (this.props.comment !== prevProps.comment) { this.render(); return; } // 处理回复编辑器的显示/隐藏 if (isReplying !== wasReplying) { const replyContainer = this.elements.root?.querySelector(':scope > .cwd-comment-body > .cwd-reply-editor-container'); if (isReplying && replyContainer) { // 显示回复编辑器 this.replyEditor = new ReplyEditor(replyContainer, { replyToAuthor: comment.author, content: this.props.replyContent, error: this.props.replyError, submitting: this.props.submitting, onUpdate: (content) => this.handleUpdateReplyContent(content), onSubmit: () => this.handleSubmitReply(), onCancel: () => this.handleCancelReply(), onClearError: () => this.handleClearReplyError() }); this.replyEditor.render(); this.replyEditor.focus(); } else if (!isReplying && replyContainer) { // 隐藏回复编辑器 replyContainer.innerHTML = ''; this.replyEditor = null; } } else if (isReplying && this.replyEditor) { // 更新回复编辑器的 props this.replyEditor.setProps({ content: this.props.replyContent, error: this.props.replyError, submitting: this.props.submitting }); } // 递归更新嵌套回复 if (this.childCommentItems && this.childCommentItems.length > 0) { this.childCommentItems.forEach((childItem) => { childItem.setProps({ replyingTo: this.props.replyingTo, replyContent: this.props.replyContent, replyError: this.props.replyError, submitting: this.props.submitting }); }); } } handleReply() { console.log('[CommentItem] handleReply called, comment.id:', this.props.comment.id); if (this.props.onReply) { this.props.onReply(this.props.comment.id); } else { console.warn('[CommentItem] onReply callback is missing!'); } } handleSubmitReply() { if (this.props.onSubmitReply) { this.props.onSubmitReply(this.props.comment.id); } } handleCancelReply() { if (this.props.onCancelReply) { this.props.onCancelReply(); } } handleUpdateReplyContent(content) { if (this.props.onUpdateReplyContent) { this.props.onUpdateReplyContent(content); } } handleClearReplyError() { if (this.props.onClearReplyError) { this.props.onClearReplyError(); } } }