- 在配置面板和核心逻辑中新增 postSlug 字段,允许自定义评论标识符 - 更新前端配置文档,说明 postSlug 的用途和默认行为 - 当未配置 postSlug 时,默认仍使用 window.location.pathname
778 lines
23 KiB
JavaScript
778 lines
23 KiB
JavaScript
/**
|
||
* CWDComments 主类
|
||
* 使用 Shadow DOM 隔离样式
|
||
*/
|
||
|
||
import { createApiClient } from './api.js';
|
||
import { createCommentStore } from './store.js';
|
||
import { CommentForm } from '@/components/CommentForm.js';
|
||
import { CommentList } from '@/components/CommentList.js';
|
||
import { ImagePreview } from '@/components/ImagePreview.js';
|
||
import styles from '@/styles/main.css?inline';
|
||
|
||
/**
|
||
* CWDComments 评论组件主类
|
||
*/
|
||
export class CWDComments {
|
||
/**
|
||
* @param {Object} config - 配置对象
|
||
* @param {string|HTMLElement} config.el - 挂载元素选择器或 DOM 元素
|
||
* @param {string} config.apiBaseUrl - API 基础地址
|
||
* @param {'light'|'dark'} [config.theme] - 主题(可选)
|
||
* @param {number} [config.pageSize] - 每页评论数(可选,默认 20)
|
||
*
|
||
* 以下字段由组件自动推导或从后端读取,无需通过 config 传入:
|
||
* - postSlug:window.location.origin + window.location.pathname
|
||
* - postTitle:document.title 或 postSlug
|
||
* - postUrl:window.location.origin + window.location.pathname
|
||
* - avatarPrefix/adminEmail/adminBadge:通过 /api/config/comments 接口获取
|
||
*/
|
||
constructor(config) {
|
||
this.config = { ...config };
|
||
if (config.siteId) {
|
||
this.config.siteId = config.siteId;
|
||
}
|
||
if (typeof window !== 'undefined' && !this.config.postSlug) {
|
||
this.config.postSlug = window.location.pathname;
|
||
}
|
||
if (typeof document !== 'undefined') {
|
||
this.config.postTitle = document.title || this.config.postSlug;
|
||
}
|
||
if (typeof window !== 'undefined') {
|
||
this.config.postUrl = window.location.origin + window.location.pathname;
|
||
}
|
||
this.hostElement = this._resolveElement(config.el);
|
||
this.shadowRoot = null;
|
||
this.mountPoint = null;
|
||
this.commentForm = null;
|
||
this.commentList = null;
|
||
this.imagePreview = null;
|
||
this.formContainer = null;
|
||
this.customStyleElement = null;
|
||
this.store = null;
|
||
this.unsubscribe = null;
|
||
this.likeState = {
|
||
count: 0,
|
||
liked: false,
|
||
loading: false,
|
||
};
|
||
this._likeButtonEl = null;
|
||
this._likeCountEl = null;
|
||
|
||
this._mounted = false;
|
||
}
|
||
|
||
/**
|
||
* 解析挂载元素
|
||
* @private
|
||
*/
|
||
_resolveElement(el) {
|
||
if (typeof document === 'undefined') {
|
||
return null;
|
||
}
|
||
if (!el) {
|
||
return null;
|
||
}
|
||
if (typeof el === 'string') {
|
||
const element = document.querySelector(el);
|
||
if (!element || !(element instanceof HTMLElement)) {
|
||
return null;
|
||
}
|
||
return element;
|
||
}
|
||
if (el instanceof HTMLElement) {
|
||
return el;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async _loadServerConfig() {
|
||
try {
|
||
const base = this.config.apiBaseUrl;
|
||
if (!base) {
|
||
return {};
|
||
}
|
||
const apiBaseUrl = base.replace(/\/$/, '');
|
||
const res = await fetch(`${apiBaseUrl}/api/config/comments`);
|
||
if (!res.ok) {
|
||
return {};
|
||
}
|
||
const data = await res.json();
|
||
return {
|
||
adminEmail: data.adminEmail || '',
|
||
adminBadge: data.adminBadge || '',
|
||
adminEnabled: !!data.adminEnabled,
|
||
avatarPrefix: data.avatarPrefix || '',
|
||
allowedDomains: Array.isArray(data.allowedDomains) ? data.allowedDomains : [],
|
||
enableCommentLike: typeof data.enableCommentLike === 'boolean' ? data.enableCommentLike : true,
|
||
enableArticleLike: typeof data.enableArticleLike === 'boolean' ? data.enableArticleLike : true,
|
||
enableImageLightbox: typeof data.enableImageLightbox === 'boolean' ? data.enableImageLightbox : false,
|
||
commentPlaceholder:
|
||
typeof data.commentPlaceholder === 'string' ? data.commentPlaceholder : undefined,
|
||
};
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 挂载组件
|
||
*/
|
||
mount() {
|
||
if (this._mounted) {
|
||
return;
|
||
}
|
||
if (this.hostElement) {
|
||
this.shadowRoot = this.hostElement.attachShadow({ mode: 'open' });
|
||
const styleElement = document.createElement('style');
|
||
if (typeof styles === 'string') {
|
||
styleElement.textContent = styles;
|
||
} else if (styles && typeof styles === 'object' && 'default' in styles) {
|
||
styleElement.textContent = styles.default;
|
||
}
|
||
this.shadowRoot.appendChild(styleElement);
|
||
this.mountPoint = document.createElement('div');
|
||
this.mountPoint.className = 'cwd-comments-container';
|
||
this.shadowRoot.appendChild(this.mountPoint);
|
||
const theme = this.config.theme || 'light';
|
||
this.mountPoint.setAttribute('data-theme', theme);
|
||
this._applyCustomCss();
|
||
}
|
||
|
||
(async () => {
|
||
const serverConfig = await this._loadServerConfig();
|
||
if (!this._mounted) {
|
||
return;
|
||
}
|
||
|
||
// 检查域名限制
|
||
if (serverConfig.allowedDomains && serverConfig.allowedDomains.length > 0 && typeof window !== 'undefined') {
|
||
const currentHostname = window.location.hostname;
|
||
const isAllowed = serverConfig.allowedDomains.some((domain) => {
|
||
return currentHostname === domain || currentHostname.endsWith('.' + domain);
|
||
});
|
||
|
||
if (!isAllowed) {
|
||
if (this.mountPoint) {
|
||
this.mountPoint.innerHTML = `
|
||
<div style="padding: 20px; text-align: center; color: #666; font-size: 14px; border: 1px solid #eee; border-radius: 8px; background: #f9f9f9;">
|
||
当前域名 (${currentHostname}) 未获得评论组件调用授权
|
||
</div>
|
||
`;
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (serverConfig.avatarPrefix) {
|
||
this.config.avatarPrefix = serverConfig.avatarPrefix;
|
||
}
|
||
if (serverConfig.adminEmail) {
|
||
this.config.adminEmail = serverConfig.adminEmail;
|
||
}
|
||
if (serverConfig.adminEnabled) {
|
||
this.config.adminBadge = serverConfig.adminBadge;
|
||
}
|
||
this.config.requireReview = !!serverConfig.requireReview;
|
||
this.config.enableCommentLike = serverConfig.enableCommentLike;
|
||
this.config.enableArticleLike = serverConfig.enableArticleLike;
|
||
this.config.enableImageLightbox = serverConfig.enableImageLightbox;
|
||
|
||
if (this.config.enableImageLightbox === true) {
|
||
if (this.mountPoint && !this.imagePreview) {
|
||
this.imagePreview = new ImagePreview(this.mountPoint);
|
||
this.mountPoint.addEventListener('click', (e) => this._handleImageClick(e));
|
||
}
|
||
}
|
||
|
||
this.config.commentPlaceholder =
|
||
typeof serverConfig.commentPlaceholder === 'string'
|
||
? serverConfig.commentPlaceholder
|
||
: this.config.commentPlaceholder;
|
||
|
||
const api = createApiClient(this.config);
|
||
this.api = api;
|
||
if (this.hostElement && this.mountPoint) {
|
||
this.store = createCommentStore(
|
||
this.config,
|
||
api.fetchComments.bind(api),
|
||
api.submitComment.bind(api),
|
||
typeof api.likeComment === 'function' ? api.likeComment.bind(api) : undefined,
|
||
);
|
||
|
||
this.unsubscribe = this.store.store.subscribe((state, prevState) => {
|
||
this._onStateChange(state, prevState);
|
||
});
|
||
|
||
this._render();
|
||
this.store.loadComments();
|
||
}
|
||
|
||
if (this.api && typeof this.api.trackVisit === 'function') {
|
||
this.api.trackVisit();
|
||
}
|
||
|
||
if (this.api && typeof this.api.getLikeStatus === 'function') {
|
||
try {
|
||
const likeResult = await this.api.getLikeStatus();
|
||
const count = likeResult && typeof likeResult.totalLikes === 'number' ? likeResult.totalLikes : 0;
|
||
const liked = !!(likeResult && likeResult.liked);
|
||
this.likeState.count = count;
|
||
this.likeState.liked = liked;
|
||
if (this.store && typeof this.store.setLikeState === 'function') {
|
||
this.store.setLikeState(count, liked);
|
||
}
|
||
this._updateLikeButton();
|
||
} catch (e) {}
|
||
}
|
||
})();
|
||
|
||
this._mounted = true;
|
||
}
|
||
|
||
/**
|
||
* 卸载组件
|
||
*/
|
||
unmount() {
|
||
if (!this._mounted) {
|
||
return;
|
||
}
|
||
|
||
// 销毁组件
|
||
if (this.commentForm) {
|
||
this.commentForm.destroy();
|
||
this.commentForm = null;
|
||
}
|
||
|
||
if (this.commentList) {
|
||
this.commentList.destroy();
|
||
this.commentList = null;
|
||
}
|
||
|
||
if (this.imagePreview) {
|
||
// imagePreview 没有 destroy 方法,但它挂载在 shadowRoot 下,会被自动移除
|
||
this.imagePreview = null;
|
||
}
|
||
|
||
// 取消订阅
|
||
if (this.unsubscribe) {
|
||
this.unsubscribe();
|
||
this.unsubscribe = null;
|
||
}
|
||
|
||
// 移除 Shadow DOM - 通过替换所有子节点
|
||
if (this.hostElement) {
|
||
// Shadow DOM 会在清空子节点时自动移除
|
||
while (this.hostElement.firstChild) {
|
||
this.hostElement.removeChild(this.hostElement.firstChild);
|
||
}
|
||
}
|
||
|
||
this.shadowRoot = null;
|
||
this.mountPoint = null;
|
||
this.store = null;
|
||
this._mounted = false;
|
||
}
|
||
|
||
/**
|
||
* 渲染组件
|
||
* @private
|
||
*/
|
||
_render() {
|
||
if (!this.mountPoint) {
|
||
return;
|
||
}
|
||
|
||
const state = this.store.store.getState();
|
||
|
||
// 创建错误提示
|
||
const existingError = this.mountPoint.querySelector('.cwd-error-inline');
|
||
if (state.error) {
|
||
if (!existingError) {
|
||
const errorEl = document.createElement('div');
|
||
errorEl.className = 'cwd-error-inline';
|
||
errorEl.innerHTML = `
|
||
<span>${state.error}</span>
|
||
<button type="button" class="cwd-error-close" data-action="clear-error">✕</button>
|
||
`;
|
||
errorEl.querySelector('[data-action="clear-error"]').addEventListener('click', () => {
|
||
this.store.clearError();
|
||
});
|
||
this.mountPoint.insertBefore(errorEl, this.mountPoint.firstChild);
|
||
}
|
||
} else if (existingError) {
|
||
existingError.remove();
|
||
}
|
||
|
||
const existingSuccess = this.mountPoint.querySelector('.cwd-success-inline');
|
||
if (state.successMessage) {
|
||
if (!existingSuccess) {
|
||
const successEl = document.createElement('div');
|
||
successEl.className = 'cwd-success-inline';
|
||
successEl.innerHTML = `
|
||
<span>${state.successMessage}</span>
|
||
<button type="button" class="cwd-error-close" data-action="clear-success">✕</button>
|
||
`;
|
||
successEl.querySelector('[data-action="clear-success"]').addEventListener('click', () => {
|
||
this.store.clearSuccess();
|
||
});
|
||
this.mountPoint.insertBefore(successEl, this.mountPoint.firstChild);
|
||
} else {
|
||
const span = existingSuccess.querySelector('span');
|
||
if (span) {
|
||
span.textContent = state.successMessage;
|
||
}
|
||
}
|
||
} else if (existingSuccess) {
|
||
existingSuccess.remove();
|
||
}
|
||
|
||
// 创建头部点赞区域
|
||
let header = this.mountPoint.querySelector('.cwd-comments-header');
|
||
if (!header) {
|
||
header = document.createElement('div');
|
||
header.className = 'cwd-comments-header';
|
||
const showArticleLike = this.config.enableArticleLike !== false;
|
||
header.innerHTML = `
|
||
<div class="cwd-like" ${showArticleLike ? '' : 'style="display: none;"'}>
|
||
<button type="button" class="cwd-like-button" data-liked="false">
|
||
<span class="cwd-like-icon-wrapper">
|
||
<svg class="cwd-like-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="M12 21c-.4 0-.8-.1-1.1-.4L4.5 15C3 13.6 2 11.7 2 9.6 2 6.5 4.5 4 7.6 4c1.7 0 3.3.8 4.4 2.1C13.1 4.8 14.7 4 16.4 4 19.5 4 22 6.5 22 9.6c0 2.1-1 4-2.5 5.4l-6.4 5.6c-.3.3-.7.4-1.1.4z"></path>
|
||
</svg>
|
||
</span>
|
||
<div>已有 <span class="cwd-like-count">0</span>人喜欢~ </div>
|
||
</button>
|
||
</div>
|
||
`;
|
||
this.mountPoint.appendChild(header);
|
||
}
|
||
|
||
this._initLikeButton(header);
|
||
|
||
if (!this.formContainer) {
|
||
this.formContainer = document.createElement('div');
|
||
this.mountPoint.appendChild(this.formContainer);
|
||
}
|
||
|
||
// 创建评论表单(放在点赞区域下方,使用单独容器以保证顺序)
|
||
if (!this.commentForm) {
|
||
this.commentForm = new CommentForm(this.formContainer, {
|
||
form: state.form,
|
||
formErrors: state.formErrors,
|
||
submitting: state.submitting,
|
||
onSubmit: () => this._handleSubmit(),
|
||
onFieldChange: (field, value) => this.store.updateFormField(field, value),
|
||
adminEmail: this.config.adminEmail,
|
||
onVerifyAdmin: (key) => this.api.verifyAdminKey(key),
|
||
placeholder: this.config.commentPlaceholder,
|
||
});
|
||
this.commentForm.render();
|
||
}
|
||
|
||
// 创建评论数统计(放在主评论表单下方、评论列表上方)
|
||
let countHeading = this.mountPoint.querySelector('.cwd-comments-count');
|
||
if (!countHeading) {
|
||
countHeading = document.createElement('h3');
|
||
countHeading.className = 'cwd-comments-count';
|
||
countHeading.innerHTML = `
|
||
共 <span class="cwd-comments-count-number">0</span> 条评论
|
||
`;
|
||
if (this.formContainer && this.formContainer.parentNode === this.mountPoint) {
|
||
this.mountPoint.insertBefore(countHeading, this.formContainer.nextSibling);
|
||
} else {
|
||
this.mountPoint.appendChild(countHeading);
|
||
}
|
||
}
|
||
const countEl = this.mountPoint.querySelector('.cwd-comments-count-number');
|
||
if (countEl) {
|
||
countEl.textContent = state.pagination.totalCount;
|
||
}
|
||
|
||
// 创建评论列表
|
||
if (!this.commentList) {
|
||
const listContainer = document.createElement('div');
|
||
this.mountPoint.appendChild(listContainer);
|
||
|
||
this.commentList = new CommentList(listContainer, {
|
||
comments: state.comments,
|
||
loading: state.loading,
|
||
error: null,
|
||
currentPage: state.pagination.page,
|
||
totalPages: this.store.getTotalPages(),
|
||
replyingTo: state.replyingTo,
|
||
replyContent: state.replyContent,
|
||
replyError: state.replyError,
|
||
submitting: state.submitting,
|
||
currentUser: state.form,
|
||
onUpdateUserInfo: (field, value) => this.store.updateFormField(field, value),
|
||
// adminEmail 已移除,前端展示改用 isAdmin 字段
|
||
adminBadge: this.config.adminBadge,
|
||
enableCommentLike: this.config.enableCommentLike !== false,
|
||
onRetry: () => this.store.loadComments(),
|
||
onReply: (commentId) => this.store.startReply(commentId),
|
||
onSubmitReply: (commentId) => this.store.submitReply(commentId),
|
||
onCancelReply: () => this.store.cancelReply(),
|
||
onUpdateReplyContent: (content) => this.store.updateReplyContent(content),
|
||
onClearReplyError: () => this.store.clearReplyError(),
|
||
replyPlaceholder: this.config.commentPlaceholder,
|
||
onPrevPage: () => this.store.goToPage(state.pagination.page - 1),
|
||
onNextPage: () => this.store.goToPage(state.pagination.page + 1),
|
||
onGoToPage: (page) => this.store.goToPage(page),
|
||
onLikeComment: (commentId, isLike) => {
|
||
if (this.store && typeof this.store.likeComment === 'function') {
|
||
this.store.likeComment(commentId, isLike);
|
||
}
|
||
},
|
||
});
|
||
this.commentList.render();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 状态变化处理
|
||
* @private
|
||
*/
|
||
_onStateChange(state, prevState) {
|
||
if (!this._mounted) {
|
||
return;
|
||
}
|
||
|
||
// 根据回复状态显示/隐藏主评论表单
|
||
if (this.commentForm?.elements?.root) {
|
||
const formRoot = this.commentForm.elements.root;
|
||
if (state.replyingTo !== null) {
|
||
formRoot.style.display = 'none';
|
||
} else {
|
||
formRoot.style.display = '';
|
||
}
|
||
}
|
||
|
||
// 更新评论表单
|
||
if (this.commentForm) {
|
||
this.commentForm.setProps({
|
||
form: state.form,
|
||
formErrors: state.formErrors,
|
||
submitting: state.submitting,
|
||
adminEmail: this.config.adminEmail,
|
||
});
|
||
}
|
||
|
||
// 更新错误提示
|
||
const existingError = this.mountPoint?.querySelector('.cwd-error-inline');
|
||
if (state.error) {
|
||
if (!existingError) {
|
||
const errorEl = document.createElement('div');
|
||
errorEl.className = 'cwd-error-inline';
|
||
errorEl.innerHTML = `
|
||
<span>${state.error}</span>
|
||
<button type="button" class="cwd-error-close" data-action="clear-error">✕</button>
|
||
`;
|
||
errorEl.querySelector('[data-action="clear-error"]').addEventListener('click', () => {
|
||
this.store.clearError();
|
||
});
|
||
this.mountPoint?.insertBefore(errorEl, this.mountPoint.firstChild);
|
||
}
|
||
} else if (existingError) {
|
||
existingError.remove();
|
||
}
|
||
|
||
const existingSuccess = this.mountPoint?.querySelector('.cwd-success-inline');
|
||
if (state.successMessage) {
|
||
if (!existingSuccess) {
|
||
const successEl = document.createElement('div');
|
||
successEl.className = 'cwd-success-inline';
|
||
successEl.innerHTML = `
|
||
<span>${state.successMessage}</span>
|
||
<button type="button" class="cwd-error-close" data-action="clear-success">✕</button>
|
||
`;
|
||
successEl.querySelector('[data-action="clear-success"]').addEventListener('click', () => {
|
||
this.store.clearSuccess();
|
||
});
|
||
this.mountPoint?.insertBefore(successEl, this.mountPoint.firstChild);
|
||
} else {
|
||
const span = existingSuccess.querySelector('span');
|
||
if (span) {
|
||
span.textContent = state.successMessage;
|
||
}
|
||
}
|
||
} else if (existingSuccess) {
|
||
existingSuccess.remove();
|
||
}
|
||
|
||
// 更新点赞和评论数统计
|
||
const header = this.mountPoint?.querySelector('.cwd-comments-header');
|
||
const countEl = this.mountPoint?.querySelector('.cwd-comments-count-number');
|
||
if (countEl) {
|
||
countEl.textContent = state.pagination.totalCount;
|
||
}
|
||
|
||
if (typeof state.likeCount === 'number' || typeof state.liked === 'boolean') {
|
||
if (typeof state.likeCount === 'number') {
|
||
this.likeState.count = state.likeCount;
|
||
}
|
||
if (typeof state.liked === 'boolean') {
|
||
this.likeState.liked = state.liked;
|
||
}
|
||
if (header) {
|
||
if (!this._likeButtonEl || !this._likeCountEl) {
|
||
this._initLikeButton(header);
|
||
}
|
||
this._updateLikeButton();
|
||
}
|
||
}
|
||
|
||
// 更新评论列表
|
||
if (this.commentList) {
|
||
this.commentList.setProps({
|
||
comments: state.comments,
|
||
loading: state.loading,
|
||
currentPage: state.pagination.page,
|
||
totalPages: this.store.getTotalPages(),
|
||
replyingTo: state.replyingTo,
|
||
replyContent: state.replyContent,
|
||
replyError: state.replyError,
|
||
submitting: state.submitting,
|
||
currentUser: state.form,
|
||
});
|
||
}
|
||
|
||
const pageChanged =
|
||
prevState &&
|
||
prevState.pagination &&
|
||
state &&
|
||
state.pagination &&
|
||
state.pagination.page !== prevState.pagination.page;
|
||
|
||
if (pageChanged) {
|
||
this._scrollToCommentsTop();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理评论提交
|
||
* @private
|
||
*/
|
||
async _handleSubmit() {
|
||
const success = await this.store.submitNewComment();
|
||
if (success) {
|
||
// 表单内容已在 store 中清空
|
||
// 更新表单组件
|
||
if (this.commentForm) {
|
||
this.commentForm.state.localForm = { ...this.store.store.getState().form };
|
||
this.commentForm.state.showPreview = false;
|
||
this.commentForm.render();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新配置
|
||
* @param {Object} newConfig - 新配置
|
||
*/
|
||
updateConfig(newConfig) {
|
||
const prevConfig = { ...this.config };
|
||
|
||
Object.assign(this.config, newConfig);
|
||
if (newConfig.siteId !== undefined) {
|
||
this.config.siteId = newConfig.siteId;
|
||
}
|
||
if (typeof window !== 'undefined' && !this.config.postSlug) {
|
||
this.config.postSlug = window.location.pathname;
|
||
}
|
||
if (typeof document !== 'undefined') {
|
||
this.config.postTitle = document.title || this.config.postSlug;
|
||
}
|
||
if (typeof window !== 'undefined') {
|
||
this.config.postUrl = window.location.origin + window.location.pathname;
|
||
}
|
||
|
||
// 更新主题
|
||
if (newConfig.theme && this.mountPoint) {
|
||
this.mountPoint.setAttribute('data-theme', newConfig.theme);
|
||
}
|
||
|
||
const shouldReload =
|
||
this.config.apiBaseUrl !== prevConfig.apiBaseUrl ||
|
||
this.config.pageSize !== prevConfig.pageSize ||
|
||
this.config.postSlug !== prevConfig.postSlug ||
|
||
this.config.siteId !== prevConfig.siteId;
|
||
|
||
if (shouldReload) {
|
||
const api = createApiClient(this.config);
|
||
this.api = api;
|
||
|
||
if (this.unsubscribe) {
|
||
this.unsubscribe();
|
||
}
|
||
|
||
this.store = createCommentStore(this.config, api.fetchComments.bind(api), api.submitComment.bind(api));
|
||
|
||
this.unsubscribe = this.store.store.subscribe((state, prevState) => {
|
||
this._onStateChange(state, prevState);
|
||
});
|
||
|
||
this.store.loadComments();
|
||
}
|
||
this._applyCustomCss();
|
||
}
|
||
|
||
_scrollToCommentsTop() {
|
||
if (typeof window === 'undefined') {
|
||
return;
|
||
}
|
||
const target = this.hostElement || this.mountPoint;
|
||
if (!target || typeof target.scrollIntoView !== 'function') {
|
||
return;
|
||
}
|
||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
_applyCustomCss() {
|
||
if (!this.shadowRoot) {
|
||
return;
|
||
}
|
||
const rawUrl = this.config && typeof this.config.customCssUrl === 'string' ? this.config.customCssUrl : '';
|
||
const url = rawUrl.trim();
|
||
if (!url) {
|
||
if (this.customStyleElement && this.customStyleElement.parentNode) {
|
||
this.customStyleElement.parentNode.removeChild(this.customStyleElement);
|
||
}
|
||
this.customStyleElement = null;
|
||
return;
|
||
}
|
||
if (!this.customStyleElement) {
|
||
const link = document.createElement('link');
|
||
link.rel = 'stylesheet';
|
||
link.href = url;
|
||
this.shadowRoot.appendChild(link);
|
||
this.customStyleElement = link;
|
||
return;
|
||
}
|
||
this.customStyleElement.href = url;
|
||
if (this.customStyleElement.parentNode !== this.shadowRoot) {
|
||
this.shadowRoot.appendChild(this.customStyleElement);
|
||
}
|
||
}
|
||
|
||
_initLikeButton(header) {
|
||
if (!header) {
|
||
return;
|
||
}
|
||
if (!this._likeButtonEl) {
|
||
this._likeButtonEl = header.querySelector('.cwd-like-button');
|
||
if (this._likeButtonEl) {
|
||
this._likeButtonEl.addEventListener('click', () => {
|
||
this._handleLikeClick();
|
||
});
|
||
}
|
||
}
|
||
if (!this._likeCountEl) {
|
||
this._likeCountEl = header.querySelector('.cwd-like-count');
|
||
}
|
||
this._updateLikeButton();
|
||
}
|
||
|
||
_updateLikeButton(animate = false) {
|
||
if (!this._likeButtonEl) {
|
||
if (!this.mountPoint) {
|
||
return;
|
||
}
|
||
const header = this.mountPoint.querySelector('.cwd-comments-header');
|
||
if (!header) {
|
||
return;
|
||
}
|
||
this._initLikeButton(header);
|
||
}
|
||
if (!this._likeButtonEl) {
|
||
return;
|
||
}
|
||
const state = this.store?.store?.getState();
|
||
const liked = state ? !!state.liked : this.likeState.liked;
|
||
const count = state && typeof state.likeCount === 'number' ? state.likeCount : this.likeState.count;
|
||
this.likeState.count = count;
|
||
this.likeState.liked = liked;
|
||
this._likeButtonEl.dataset.liked = liked ? 'true' : 'false';
|
||
this._likeButtonEl.dataset.loading = this.likeState.loading ? 'true' : 'false';
|
||
if (this._likeCountEl) {
|
||
this._likeCountEl.textContent = String(count);
|
||
}
|
||
if (animate && this._likeButtonEl) {
|
||
this._likeButtonEl.classList.remove('cwd-like-animate');
|
||
void this._likeButtonEl.offsetWidth;
|
||
this._likeButtonEl.classList.add('cwd-like-animate');
|
||
}
|
||
}
|
||
|
||
_handleLikeClick() {
|
||
if (!this.api || typeof this.api.likePage !== 'function') {
|
||
return;
|
||
}
|
||
if (this.likeState.loading) {
|
||
return;
|
||
}
|
||
const currentState = this.store?.store?.getState();
|
||
const currentCount = currentState && typeof currentState.likeCount === 'number' ? currentState.likeCount : this.likeState.count;
|
||
const wasLiked = currentState ? !!currentState.liked : this.likeState.liked;
|
||
if (wasLiked) {
|
||
return;
|
||
}
|
||
const nextCount = currentCount + 1;
|
||
this.likeState.loading = true;
|
||
this.likeState.count = nextCount;
|
||
this.likeState.liked = true;
|
||
if (this.store && typeof this.store.setLikeState === 'function') {
|
||
this.store.setLikeState(nextCount, true);
|
||
}
|
||
this._updateLikeButton(true);
|
||
this.api
|
||
.likePage()
|
||
.then((result) => {
|
||
const total = result && typeof result.totalLikes === 'number' ? result.totalLikes : nextCount;
|
||
const liked = !!(result && result.liked);
|
||
this.likeState.count = total;
|
||
this.likeState.liked = liked;
|
||
if (this.store && typeof this.store.setLikeState === 'function') {
|
||
this.store.setLikeState(total, liked);
|
||
}
|
||
this._updateLikeButton();
|
||
})
|
||
.catch(() => {
|
||
this.likeState.count = currentCount;
|
||
this.likeState.liked = wasLiked;
|
||
if (this.store && typeof this.store.setLikeState === 'function') {
|
||
this.store.setLikeState(currentCount, wasLiked);
|
||
}
|
||
this._updateLikeButton();
|
||
})
|
||
.finally(() => {
|
||
this.likeState.loading = false;
|
||
this._updateLikeButton();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理图片点击
|
||
* @private
|
||
*/
|
||
_handleImageClick(e) {
|
||
const target = e.target;
|
||
// 检查点击的是否是评论内容中的图片
|
||
if (target.tagName === 'IMG' && target.closest('.cwd-comment-content')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (this.imagePreview) {
|
||
this.imagePreview.open(target.src);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前配置
|
||
* @returns {Object}
|
||
*/
|
||
getConfig() {
|
||
return { ...this.config };
|
||
}
|
||
}
|