feat: 新增评论框提示文案自定义功能并优化侧边栏排序
- 新增评论框提示文案(placeholder)自定义功能,支持在后台设置 - 更新文档:重命名“点赞开关”为“功能开关”,调整侧边栏排序 - 优化前端组件,统一处理评论表单和回复编辑器的提示文案 - 修复CSS样式,确保操作按钮始终可见
This commit is contained in:
@@ -126,6 +126,7 @@ export type LikeStatsResponse = {
|
|||||||
export type FeatureSettingsResponse = {
|
export type FeatureSettingsResponse = {
|
||||||
enableCommentLike: boolean;
|
enableCommentLike: boolean;
|
||||||
enableArticleLike: boolean;
|
enableArticleLike: boolean;
|
||||||
|
commentPlaceholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminDisplaySettingsResponse = {
|
export type AdminDisplaySettingsResponse = {
|
||||||
@@ -327,7 +328,7 @@ export function fetchFeatureSettings(): Promise<FeatureSettingsResponse> {
|
|||||||
return get<FeatureSettingsResponse>('/admin/settings/features');
|
return get<FeatureSettingsResponse>('/admin/settings/features');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveFeatureSettings(data: { enableCommentLike?: boolean; enableArticleLike?: boolean }): Promise<{ message: string }> {
|
export function saveFeatureSettings(data: { enableCommentLike?: boolean; enableArticleLike?: boolean; commentPlaceholder?: string }): Promise<{ message: string }> {
|
||||||
return put<{ message: string }>('/admin/settings/features', data);
|
return put<{ message: string }>('/admin/settings/features', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,20 @@
|
|||||||
开启后,评论列表中的每条评论都会显示点赞按钮。
|
开启后,评论列表中的每条评论都会显示点赞按钮。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label class="form-label">评论框提示文案 (Placeholder)</label>
|
||||||
|
<textarea
|
||||||
|
v-model="commentPlaceholder"
|
||||||
|
class="form-input"
|
||||||
|
rows="3"
|
||||||
|
style="height:90px;resize:none;"
|
||||||
|
placeholder="默认:写下你的评论,可换行书写提示"
|
||||||
|
></textarea>
|
||||||
|
<div class="form-hint">
|
||||||
|
自定义评论输入框的提示文字,支持换行。留空则使用默认值。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button
|
<button
|
||||||
class="card-button"
|
class="card-button"
|
||||||
@@ -600,6 +614,7 @@ const adminKeySet = ref(false);
|
|||||||
const requireReview = ref(false);
|
const requireReview = ref(false);
|
||||||
const enableArticleLike = ref(true);
|
const enableArticleLike = ref(true);
|
||||||
const enableCommentLike = ref(true);
|
const enableCommentLike = ref(true);
|
||||||
|
const commentPlaceholder = ref("");
|
||||||
const telegramBotToken = ref("");
|
const telegramBotToken = ref("");
|
||||||
const telegramChatId = ref("");
|
const telegramChatId = ref("");
|
||||||
const telegramNotifyEnabled = ref(false);
|
const telegramNotifyEnabled = ref(false);
|
||||||
@@ -853,6 +868,7 @@ async function load() {
|
|||||||
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
|
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
|
||||||
enableArticleLike.value = featureRes.enableArticleLike;
|
enableArticleLike.value = featureRes.enableArticleLike;
|
||||||
enableCommentLike.value = featureRes.enableCommentLike;
|
enableCommentLike.value = featureRes.enableCommentLike;
|
||||||
|
commentPlaceholder.value = featureRes.commentPlaceholder || "";
|
||||||
|
|
||||||
telegramBotToken.value = telegramRes.botToken || "";
|
telegramBotToken.value = telegramRes.botToken || "";
|
||||||
telegramChatId.value = telegramRes.chatId || "";
|
telegramChatId.value = telegramRes.chatId || "";
|
||||||
@@ -998,6 +1014,7 @@ async function saveFeature() {
|
|||||||
saveFeatureSettings({
|
saveFeatureSettings({
|
||||||
enableArticleLike: enableArticleLike.value,
|
enableArticleLike: enableArticleLike.value,
|
||||||
enableCommentLike: enableCommentLike.value,
|
enableCommentLike: enableCommentLike.value,
|
||||||
|
commentPlaceholder: commentPlaceholder.value,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,15 @@ export const updateFeatureSettings = async (c: Context<{ Bindings: Bindings }>)
|
|||||||
typeof body.enableArticleLike === 'boolean'
|
typeof body.enableArticleLike === 'boolean'
|
||||||
? body.enableArticleLike
|
? body.enableArticleLike
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const rawCommentPlaceholder =
|
||||||
|
typeof body.commentPlaceholder === 'string' ? body.commentPlaceholder : undefined;
|
||||||
|
const commentPlaceholder =
|
||||||
|
rawCommentPlaceholder !== undefined ? rawCommentPlaceholder.trim() : undefined;
|
||||||
|
|
||||||
await saveFeatureSettings(c.env, {
|
await saveFeatureSettings(c.env, {
|
||||||
enableCommentLike,
|
enableCommentLike,
|
||||||
enableArticleLike
|
enableArticleLike,
|
||||||
|
commentPlaceholder
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({ message: '保存成功!' });
|
return c.json({ message: '保存成功!' });
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { Bindings } from '../bindings';
|
|||||||
|
|
||||||
export const FEATURE_COMMENT_LIKE_KEY = 'comment_feature_comment_like';
|
export const FEATURE_COMMENT_LIKE_KEY = 'comment_feature_comment_like';
|
||||||
export const FEATURE_ARTICLE_LIKE_KEY = 'comment_feature_article_like';
|
export const FEATURE_ARTICLE_LIKE_KEY = 'comment_feature_article_like';
|
||||||
|
export const FEATURE_COMMENT_PLACEHOLDER_KEY = 'comment_feature_placeholder';
|
||||||
|
|
||||||
export type FeatureSettings = {
|
export type FeatureSettings = {
|
||||||
enableCommentLike: boolean;
|
enableCommentLike: boolean;
|
||||||
enableArticleLike: boolean;
|
enableArticleLike: boolean;
|
||||||
|
commentPlaceholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadFeatureSettings(env: Bindings): Promise<FeatureSettings> {
|
export async function loadFeatureSettings(env: Bindings): Promise<FeatureSettings> {
|
||||||
@@ -13,9 +15,9 @@ export async function loadFeatureSettings(env: Bindings): Promise<FeatureSetting
|
|||||||
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
|
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
const keys = [FEATURE_COMMENT_LIKE_KEY, FEATURE_ARTICLE_LIKE_KEY];
|
const keys = [FEATURE_COMMENT_LIKE_KEY, FEATURE_ARTICLE_LIKE_KEY, FEATURE_COMMENT_PLACEHOLDER_KEY];
|
||||||
const { results } = await env.CWD_DB.prepare(
|
const { results } = await env.CWD_DB.prepare(
|
||||||
'SELECT key, value FROM Settings WHERE key IN (?, ?)'
|
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?)'
|
||||||
)
|
)
|
||||||
.bind(...keys)
|
.bind(...keys)
|
||||||
.all<{ key: string; value: string }>();
|
.all<{ key: string; value: string }>();
|
||||||
@@ -47,9 +49,12 @@ export async function loadFeatureSettings(env: Bindings): Promise<FeatureSetting
|
|||||||
const enableArticleLikeRaw = map.get(FEATURE_ARTICLE_LIKE_KEY);
|
const enableArticleLikeRaw = map.get(FEATURE_ARTICLE_LIKE_KEY);
|
||||||
const enableArticleLike = enableArticleLikeRaw !== '0'; // Default to true if missing or '1'
|
const enableArticleLike = enableArticleLikeRaw !== '0'; // Default to true if missing or '1'
|
||||||
|
|
||||||
|
const commentPlaceholder = map.get(FEATURE_COMMENT_PLACEHOLDER_KEY);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enableCommentLike,
|
enableCommentLike,
|
||||||
enableArticleLike
|
enableArticleLike,
|
||||||
|
commentPlaceholder
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,10 @@ export async function saveFeatureSettings(
|
|||||||
? '1'
|
? '1'
|
||||||
: '0'
|
: '0'
|
||||||
: undefined
|
: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FEATURE_COMMENT_PLACEHOLDER_KEY,
|
||||||
|
value: settings.commentPlaceholder
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ export const rootSidebar = [
|
|||||||
text: '功能',
|
text: '功能',
|
||||||
items: [
|
items: [
|
||||||
{ text: '后台设置', link: '/function/admin-panel' },
|
{ text: '后台设置', link: '/function/admin-panel' },
|
||||||
{ text: '通知邮箱', link: '/function/email-reminder' },
|
{ text: '邮箱提醒', link: '/function/email-reminder' },
|
||||||
{ text: 'Telegram 通知', link: '/function/telegram-notify' },
|
{ text: 'Telegram 通知', link: '/function/telegram-notify' },
|
||||||
{ text: '安全设置', link: '/function/security-settings' },
|
{ text: '安全设置', link: '/function/security-settings' },
|
||||||
|
{ text: '功能开关', link: '/function/feature-settings' },
|
||||||
|
{ text: '数据看板', link: '/function/data-statistics' },
|
||||||
{ text: '数据管理', link: '/function/data-migration' },
|
{ text: '数据管理', link: '/function/data-migration' },
|
||||||
{ text: '点赞开关', link: '/function/feature-settings' },
|
|
||||||
{ text: '数据统计', link: '/function/data-statistics' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ text: '反馈', link: '/guide/feedback' },
|
{ text: '反馈', link: '/guide/feedback' },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 数据迁移
|
# 数据管理
|
||||||
|
|
||||||
目前支持迁移数据的评论框架有以下,请保证导出的格式为 json 格式。
|
目前支持迁移数据的评论框架有以下,请保证导出的格式为 json 格式。
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 数据统计
|
# 数据看板
|
||||||
|
|
||||||
CWD 内置提供评论数据分析和访客数据分析功能。
|
CWD 内置提供评论数据分析和访客数据分析功能。
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 通知邮箱
|
# 邮箱提醒
|
||||||
|
|
||||||
目前支持进行通知的邮箱有:
|
目前支持进行通知的邮箱有:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 点赞开关
|
# 功能开关
|
||||||
|
|
||||||
|
### 点赞开关
|
||||||
|
|
||||||
在 `网站设置` > `功能开关` 中,您可以控制文章点赞和评论点赞功能的开启状态。
|
在 `网站设置` > `功能开关` 中,您可以控制文章点赞和评论点赞功能的开启状态。
|
||||||
|
|
||||||
功能默认开启,可根据需要关闭。
|
功能默认开启,可根据需要关闭。
|
||||||
|
|
||||||
|
### 评论框提示文案 (Placeholder)
|
||||||
|
|
||||||
|
自定义评论输入框的提示文字,支持换行。留空则使用默认值。
|
||||||
@@ -43,6 +43,7 @@ export class CommentForm extends Component {
|
|||||||
const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
|
const canSubmit = localForm.name.trim() && localForm.email.trim() && localForm.content.trim();
|
||||||
const isAdmin = this.props.adminEmail && localForm.email.trim() === this.props.adminEmail;
|
const isAdmin = this.props.adminEmail && localForm.email.trim() === this.props.adminEmail;
|
||||||
const isVerified = isAdmin && auth.hasToken();
|
const isVerified = isAdmin && auth.hasToken();
|
||||||
|
const placeholderText = this.props.placeholder || '';
|
||||||
|
|
||||||
const root = this.createElement('form', {
|
const root = this.createElement('form', {
|
||||||
className: 'cwd-comment-form',
|
className: 'cwd-comment-form',
|
||||||
@@ -104,6 +105,7 @@ export class CommentForm extends Component {
|
|||||||
className: `cwd-form-textarea ${formErrors.content ? 'cwd-input-error' : ''}`,
|
className: `cwd-form-textarea ${formErrors.content ? 'cwd-input-error' : ''}`,
|
||||||
attributes: {
|
attributes: {
|
||||||
rows: 4,
|
rows: 4,
|
||||||
|
placeholder: placeholderText,
|
||||||
disabled: submitting,
|
disabled: submitting,
|
||||||
onInput: (e) => this.handleFieldChange('content', e.target.value),
|
onInput: (e) => this.handleFieldChange('content', e.target.value),
|
||||||
onKeydown: (e) => this.handleContentKeydown(e),
|
onKeydown: (e) => this.handleContentKeydown(e),
|
||||||
|
|||||||
@@ -7,453 +7,458 @@ import { ReplyEditor } from './ReplyEditor.js';
|
|||||||
import { formatRelativeTime } from '@/utils/date.js';
|
import { formatRelativeTime } from '@/utils/date.js';
|
||||||
|
|
||||||
export class CommentItem extends Component {
|
export class CommentItem extends Component {
|
||||||
// 防抖缓存,防止连续点击
|
// 防抖缓存,防止连续点击
|
||||||
static _likeDebounce = new Map();
|
static _likeDebounce = new Map();
|
||||||
// 用户标识缓存(单例,确保一致性)
|
// 用户标识缓存(单例,确保一致性)
|
||||||
static _userId = null;
|
static _userId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement|string} container - 容器元素或选择器
|
* @param {HTMLElement|string} container - 容器元素或选择器
|
||||||
* @param {Object} props - 组件属性
|
* @param {Object} props - 组件属性
|
||||||
* @param {Object} props.comment - 评论数据
|
* @param {Object} props.comment - 评论数据
|
||||||
* @param {boolean} props.isReply - 是否为回复
|
* @param {boolean} props.isReply - 是否为回复
|
||||||
* @param {number|null} props.replyingTo - 当前正在回复的评论 ID
|
* @param {number|null} props.replyingTo - 当前正在回复的评论 ID
|
||||||
* @param {string} props.replyContent - 回复内容
|
* @param {string} props.replyContent - 回复内容
|
||||||
* @param {string|null} props.replyError - 回复错误
|
* @param {string|null} props.replyError - 回复错误
|
||||||
* @param {boolean} props.submitting - 是否正在提交
|
* @param {boolean} props.submitting - 是否正在提交
|
||||||
* @param {string} props.adminEmail - 博主邮箱(可选)
|
* @param {string} props.adminEmail - 博主邮箱(可选)
|
||||||
* @param {string} props.adminBadge - 博主标识文字(可选)
|
* @param {string} props.adminBadge - 博主标识文字(可选)
|
||||||
* @param {Function} props.onReply - 回复回调
|
* @param {Function} props.onReply - 回复回调
|
||||||
* @param {Function} props.onSubmitReply - 提交回复回调
|
* @param {Function} props.onSubmitReply - 提交回复回调
|
||||||
* @param {Function} props.onCancelReply - 取消回复回调
|
* @param {Function} props.onCancelReply - 取消回复回调
|
||||||
* @param {Function} props.onUpdateReplyContent - 更新回复内容回调
|
* @param {Function} props.onUpdateReplyContent - 更新回复内容回调
|
||||||
* @param {Function} props.onClearReplyError - 清除回复错误回调
|
* @param {Function} props.onClearReplyError - 清除回复错误回调
|
||||||
*/
|
*/
|
||||||
constructor(container, props = {}) {
|
constructor(container, props = {}) {
|
||||||
super(container, props);
|
super(container, props);
|
||||||
this.replyEditor = null;
|
this.replyEditor = null;
|
||||||
this.childCommentItems = []; // 缓存嵌套回复的 CommentItem 实例
|
this.childCommentItems = []; // 缓存嵌套回复的 CommentItem 实例
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { comment, isReply, adminEmail, adminBadge } = this.props;
|
const { comment, isReply, adminEmail, adminBadge } = this.props;
|
||||||
const isPinned = typeof comment.priority === 'number' && comment.priority > 1;
|
const isPinned = typeof comment.priority === 'number' && comment.priority > 1;
|
||||||
const isReplying = this.props.replyingTo === comment.id;
|
const isReplying = this.props.replyingTo === comment.id;
|
||||||
// 使用后端返回的 isAdmin 字段,不再依赖前端邮箱比对
|
// 使用后端返回的 isAdmin 字段,不再依赖前端邮箱比对
|
||||||
const isAdmin = !!comment.isAdmin;
|
const isAdmin = !!comment.isAdmin;
|
||||||
|
|
||||||
const root = this.createElement('div', {
|
const root = this.createElement('div', {
|
||||||
className: `cwd-comment-item ${isReply ? 'cwd-comment-reply' : ''}`,
|
className: `cwd-comment-item ${isReply ? 'cwd-comment-reply' : ''}`,
|
||||||
children: [
|
children: [
|
||||||
// 头像
|
// 头像
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-avatar',
|
className: 'cwd-comment-avatar',
|
||||||
children: [
|
children: [
|
||||||
this.createElement('img', {
|
this.createElement('img', {
|
||||||
attributes: {
|
attributes: {
|
||||||
src: comment.avatar,
|
src: comment.avatar,
|
||||||
alt: comment.name,
|
alt: comment.name,
|
||||||
loading: 'lazy'
|
loading: 'lazy',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 主体内容
|
// 主体内容
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-body',
|
className: 'cwd-comment-body',
|
||||||
children: [
|
children: [
|
||||||
// 头部(作者名、操作按钮、时间)
|
// 头部(作者名、操作按钮、时间)
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-header',
|
className: 'cwd-comment-header',
|
||||||
children: [
|
children: [
|
||||||
// 作者信息
|
// 作者信息
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-author',
|
className: 'cwd-comment-author',
|
||||||
children: [
|
children: [
|
||||||
comment.url
|
comment.url
|
||||||
? this.createElement('span', {
|
? this.createElement('span', {
|
||||||
className: 'cwd-author-name',
|
className: 'cwd-author-name',
|
||||||
children: [
|
children: [
|
||||||
this.createElement('a', {
|
this.createElement('a', {
|
||||||
attributes: {
|
attributes: {
|
||||||
href: comment.url,
|
href: comment.url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
rel: 'noopener noreferrer'
|
rel: 'noopener noreferrer',
|
||||||
},
|
},
|
||||||
text: comment.name
|
text: comment.name,
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
: this.createTextElement('span', comment.name, 'cwd-author-name'),
|
: this.createTextElement('span', comment.name, 'cwd-author-name'),
|
||||||
...(isAdmin ? [
|
...(isAdmin
|
||||||
adminBadge
|
? [
|
||||||
? this.createTextElement('span', `${adminBadge}`, 'cwd-admin-badge')
|
adminBadge
|
||||||
: this.createElement('span', {
|
? this.createTextElement('span', `${adminBadge}`, 'cwd-admin-badge')
|
||||||
className: 'cwd-admin-badge cwd-admin-badge-icon',
|
: this.createElement('span', {
|
||||||
attributes: {
|
className: 'cwd-admin-badge cwd-admin-badge-icon',
|
||||||
title: '网站管理员'
|
attributes: {
|
||||||
},
|
title: '网站管理员',
|
||||||
html: '<svg viewBox="0 0 22 22" aria-label="网站管理员" role="img" class="cwd-admin-icon" style="width:15px;height:15px;fill:currentColor;vertical-align:-0.15em;"><g><path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z"></path></g></svg>'
|
},
|
||||||
})
|
html: '<svg viewBox="0 0 22 22" aria-label="网站管理员" role="img" class="cwd-admin-icon" style="width:15px;height:15px;fill:currentColor;vertical-align:-0.15em;"><g><path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z"></path></g></svg>',
|
||||||
] : []),
|
}),
|
||||||
...(isPinned ? [
|
]
|
||||||
this.createTextElement('span', '置顶', 'cwd-pin-badge')
|
: []),
|
||||||
] : []),
|
...(isPinned ? [this.createTextElement('span', '置顶', 'cwd-pin-badge')] : []),
|
||||||
// 显示回复目标
|
// 显示回复目标
|
||||||
...(comment.replyToAuthor ? [
|
...(comment.replyToAuthor
|
||||||
this.createTextElement('span', ' 回复 ', 'cwd-reply-to-separator'),
|
? [
|
||||||
this.createTextElement('span', comment.replyToAuthor, 'cwd-reply-to-author')
|
this.createTextElement('span', ' 回复 ', 'cwd-reply-to-separator'),
|
||||||
] : [])
|
this.createTextElement('span', comment.replyToAuthor, 'cwd-reply-to-author'),
|
||||||
]
|
]
|
||||||
}),
|
: []),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
// 操作区域
|
// 操作区域
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-actions',
|
className: 'cwd-comment-actions',
|
||||||
children: [
|
children: [
|
||||||
this.createElement('span', {
|
this.createElement('span', {
|
||||||
className: 'cwd-action-btn',
|
className: 'cwd-action-btn',
|
||||||
attributes: {
|
attributes: {
|
||||||
onClick: () => this.handleReply()
|
onClick: () => this.handleReply(),
|
||||||
},
|
},
|
||||||
text: '回复'
|
text: '回复',
|
||||||
}),
|
}),
|
||||||
...(this.props.enableCommentLike !== false ? [
|
...(this.props.enableCommentLike !== false
|
||||||
this.createElement('div', {
|
? [
|
||||||
className: 'cwd-comment-like',
|
this.createElement('div', {
|
||||||
children: [
|
className: 'cwd-comment-like',
|
||||||
this.createElement('button', {
|
children: [
|
||||||
className: `cwd-comment-like-button${this.hasLiked(comment.id) ? ' cwd-comment-like-button-liked' : ''}`,
|
this.createElement('button', {
|
||||||
attributes: {
|
className: `cwd-comment-like-button${this.hasLiked(comment.id) ? ' cwd-comment-like-button-liked' : ''}`,
|
||||||
type: 'button',
|
attributes: {
|
||||||
'aria-label': this.hasLiked(comment.id) ? '取消点赞' : '点赞',
|
type: 'button',
|
||||||
onClick: () => this.handleLikeComment()
|
'aria-label': this.hasLiked(comment.id) ? '取消点赞' : '点赞',
|
||||||
},
|
onClick: () => this.handleLikeComment(),
|
||||||
children: [
|
},
|
||||||
this.createElement('span', {
|
children: [
|
||||||
className: 'cwd-comment-like-icon-wrapper',
|
this.createElement('span', {
|
||||||
children: [
|
className: 'cwd-comment-like-icon-wrapper',
|
||||||
this.createElement('svg', {
|
children: [
|
||||||
className: 'cwd-comment-like-icon',
|
this.createElement('svg', {
|
||||||
attributes: {
|
className: 'cwd-comment-like-icon',
|
||||||
viewBox: '0 0 24 24',
|
attributes: {
|
||||||
'aria-hidden': 'true',
|
viewBox: '0 0 24 24',
|
||||||
fill: this.hasLiked(comment.id) ? 'currentColor' : 'none'
|
'aria-hidden': 'true',
|
||||||
},
|
fill: this.hasLiked(comment.id) ? 'currentColor' : 'none',
|
||||||
children: [
|
},
|
||||||
this.createElement('path', {
|
children: [
|
||||||
attributes: {
|
this.createElement('path', {
|
||||||
d: 'M2 21h4V9H2v12zm20-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L13 1 7.59 6.41C7.22 6.78 7 7.3 7 7.83V19c0 1.1.9 2 2 2h8c.78 0 1.48-.45 1.82-1.11l3.02-7.05c.11-.23.16-.48.16-.74v-2z'
|
attributes: {
|
||||||
}
|
d: 'M2 21h4V9H2v12zm20-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L13 1 7.59 6.41C7.22 6.78 7 7.3 7 7.83V19c0 1.1.9 2 2 2h8c.78 0 1.48-.45 1.82-1.11l3.02-7.05c.11-.23.16-.48.16-.74v-2z',
|
||||||
})
|
},
|
||||||
]
|
}),
|
||||||
})
|
],
|
||||||
]
|
}),
|
||||||
}),
|
],
|
||||||
...(() => {
|
}),
|
||||||
const likeCount =
|
...(() => {
|
||||||
typeof comment.likes === 'number' && Number.isFinite(comment.likes) && comment.likes >= 0
|
const likeCount =
|
||||||
? comment.likes
|
typeof comment.likes === 'number' && Number.isFinite(comment.likes) && comment.likes >= 0
|
||||||
: 0;
|
? comment.likes
|
||||||
return likeCount >= 1
|
: 0;
|
||||||
? [
|
return likeCount >= 1
|
||||||
this.createTextElement(
|
? [this.createTextElement('span', String(likeCount), 'cwd-comment-like-count')]
|
||||||
'span',
|
: [];
|
||||||
String(likeCount),
|
})(),
|
||||||
'cwd-comment-like-count'
|
],
|
||||||
)
|
}),
|
||||||
]
|
],
|
||||||
: [];
|
}),
|
||||||
})()
|
]
|
||||||
]
|
: []),
|
||||||
})
|
this.createTextElement('span', formatRelativeTime(comment.created), 'cwd-comment-time'),
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
] : []),
|
],
|
||||||
this.createTextElement('span', formatRelativeTime(comment.created), 'cwd-comment-time')
|
}),
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 评论内容
|
// 评论内容
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-comment-content'
|
className: 'cwd-comment-content',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 回复编辑器容器
|
// 回复编辑器容器
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-reply-editor-container'
|
className: 'cwd-reply-editor-container',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 嵌套回复容器
|
// 嵌套回复容器
|
||||||
...(comment.replies && comment.replies.length > 0 ? [
|
...(comment.replies && comment.replies.length > 0
|
||||||
this.createElement('div', {
|
? [
|
||||||
className: 'cwd-replies'
|
this.createElement('div', {
|
||||||
})
|
className: 'cwd-replies',
|
||||||
] : [])
|
}),
|
||||||
]
|
]
|
||||||
})
|
: []),
|
||||||
]
|
],
|
||||||
});
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
// 设置评论内容的 TEXT
|
// 设置评论内容的 TEXT
|
||||||
const contentEl = root.querySelector('.cwd-comment-content');
|
const contentEl = root.querySelector('.cwd-comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
contentEl.innerHTML = comment.contentHtml;
|
contentEl.innerHTML = comment.contentHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建回复编辑器
|
// 创建回复编辑器
|
||||||
if (isReplying) {
|
if (isReplying) {
|
||||||
const replyContainer = root.querySelector('.cwd-reply-editor-container');
|
const replyContainer = root.querySelector('.cwd-reply-editor-container');
|
||||||
if (replyContainer) {
|
if (replyContainer) {
|
||||||
this.replyEditor = new ReplyEditor(replyContainer, {
|
this.replyEditor = new ReplyEditor(replyContainer, {
|
||||||
replyToAuthor: comment.name,
|
replyToAuthor: comment.name,
|
||||||
content: this.props.replyContent,
|
content: this.props.replyContent,
|
||||||
error: this.props.replyError,
|
error: this.props.replyError,
|
||||||
submitting: this.props.submitting,
|
submitting: this.props.submitting,
|
||||||
currentUser: this.props.currentUser,
|
currentUser: this.props.currentUser,
|
||||||
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
||||||
onUpdate: (content) => this.handleUpdateReplyContent(content),
|
onUpdate: (content) => this.handleUpdateReplyContent(content),
|
||||||
onSubmit: () => this.handleSubmitReply(),
|
onSubmit: () => this.handleSubmitReply(),
|
||||||
onCancel: () => this.handleCancelReply(),
|
onCancel: () => this.handleCancelReply(),
|
||||||
onClearError: () => this.handleClearReplyError()
|
onClearError: () => this.handleClearReplyError(),
|
||||||
});
|
placeholder: this.props.replyPlaceholder,
|
||||||
this.replyEditor.render();
|
});
|
||||||
this.replyEditor.focus();
|
this.replyEditor.render();
|
||||||
}
|
this.replyEditor.focus();
|
||||||
} else {
|
}
|
||||||
this.replyEditor = null;
|
} else {
|
||||||
}
|
this.replyEditor = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染嵌套回复
|
// 渲染嵌套回复
|
||||||
this.childCommentItems = [];
|
this.childCommentItems = [];
|
||||||
if (comment.replies && comment.replies.length > 0) {
|
if (comment.replies && comment.replies.length > 0) {
|
||||||
const repliesContainer = root.querySelector('.cwd-replies');
|
const repliesContainer = root.querySelector('.cwd-replies');
|
||||||
if (repliesContainer) {
|
if (repliesContainer) {
|
||||||
comment.replies.forEach(reply => {
|
comment.replies.forEach((reply) => {
|
||||||
const replyItem = new CommentItem(repliesContainer, {
|
const replyItem = new CommentItem(repliesContainer, {
|
||||||
comment: reply,
|
comment: reply,
|
||||||
isReply: true,
|
isReply: true,
|
||||||
replyingTo: this.props.replyingTo,
|
replyingTo: this.props.replyingTo,
|
||||||
replyContent: this.props.replyContent,
|
replyContent: this.props.replyContent,
|
||||||
replyError: this.props.replyError,
|
replyError: this.props.replyError,
|
||||||
submitting: this.props.submitting,
|
submitting: this.props.submitting,
|
||||||
currentUser: this.props.currentUser,
|
currentUser: this.props.currentUser,
|
||||||
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
||||||
// adminEmail 已移除
|
// adminEmail 已移除
|
||||||
adminBadge: this.props.adminBadge,
|
adminBadge: this.props.adminBadge,
|
||||||
enableCommentLike: this.props.enableCommentLike,
|
enableCommentLike: this.props.enableCommentLike,
|
||||||
onReply: this.props.onReply,
|
replyPlaceholder: this.props.replyPlaceholder,
|
||||||
onLikeComment: this.props.onLikeComment,
|
onReply: this.props.onReply,
|
||||||
onSubmitReply: this.props.onSubmitReply,
|
onLikeComment: this.props.onLikeComment,
|
||||||
onCancelReply: this.props.onCancelReply,
|
onSubmitReply: this.props.onSubmitReply,
|
||||||
onUpdateReplyContent: this.props.onUpdateReplyContent,
|
onCancelReply: this.props.onCancelReply,
|
||||||
onClearReplyError: this.props.onClearReplyError
|
onUpdateReplyContent: this.props.onUpdateReplyContent,
|
||||||
});
|
onClearReplyError: this.props.onClearReplyError,
|
||||||
replyItem.render();
|
});
|
||||||
this.childCommentItems.push(replyItem);
|
replyItem.render();
|
||||||
});
|
this.childCommentItems.push(replyItem);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.elements.root = root;
|
this.elements.root = root;
|
||||||
|
|
||||||
// 只在首次渲染时清空容器(当还没有 root 元素时)
|
// 只在首次渲染时清空容器(当还没有 root 元素时)
|
||||||
if (this.container.contains(root)) {
|
if (this.container.contains(root)) {
|
||||||
// 如果 root 已存在,替换它
|
// 如果 root 已存在,替换它
|
||||||
this.container.replaceChild(root, this.elements.root);
|
this.container.replaceChild(root, this.elements.root);
|
||||||
} else {
|
} else {
|
||||||
// 否则直接添加
|
// 否则直接添加
|
||||||
this.container.appendChild(root);
|
this.container.appendChild(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(prevProps) {
|
updateProps(prevProps) {
|
||||||
const { comment } = this.props;
|
const { comment } = this.props;
|
||||||
const wasReplying = prevProps.replyingTo === comment.id;
|
const wasReplying = prevProps.replyingTo === comment.id;
|
||||||
const isReplying = this.props.replyingTo === comment.id;
|
const isReplying = this.props.replyingTo === comment.id;
|
||||||
|
|
||||||
// 如果评论数据本身变化,需要完全重新渲染
|
// 如果评论数据本身变化,需要完全重新渲染
|
||||||
if (this.props.comment !== prevProps.comment) {
|
if (this.props.comment !== prevProps.comment) {
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理回复编辑器的显示/隐藏
|
// 处理回复编辑器的显示/隐藏
|
||||||
if (isReplying !== wasReplying) {
|
if (isReplying !== wasReplying) {
|
||||||
const replyContainer = this.elements.root?.querySelector(':scope > .cwd-comment-body > .cwd-reply-editor-container');
|
const replyContainer = this.elements.root?.querySelector(':scope > .cwd-comment-body > .cwd-reply-editor-container');
|
||||||
if (isReplying && replyContainer) {
|
if (isReplying && replyContainer) {
|
||||||
// 显示回复编辑器
|
// 显示回复编辑器
|
||||||
this.replyEditor = new ReplyEditor(replyContainer, {
|
this.replyEditor = new ReplyEditor(replyContainer, {
|
||||||
replyToAuthor: comment.name,
|
replyToAuthor: comment.name,
|
||||||
content: this.props.replyContent,
|
content: this.props.replyContent,
|
||||||
error: this.props.replyError,
|
error: this.props.replyError,
|
||||||
submitting: this.props.submitting,
|
submitting: this.props.submitting,
|
||||||
currentUser: this.props.currentUser,
|
currentUser: this.props.currentUser,
|
||||||
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
onUpdateUserInfo: this.props.onUpdateUserInfo,
|
||||||
onUpdate: (content) => this.handleUpdateReplyContent(content),
|
onUpdate: (content) => this.handleUpdateReplyContent(content),
|
||||||
onSubmit: () => this.handleSubmitReply(),
|
onSubmit: () => this.handleSubmitReply(),
|
||||||
onCancel: () => this.handleCancelReply(),
|
onCancel: () => this.handleCancelReply(),
|
||||||
onClearError: () => this.handleClearReplyError()
|
onClearError: () => this.handleClearReplyError(),
|
||||||
});
|
placeholder: this.props.replyPlaceholder,
|
||||||
this.replyEditor.render();
|
});
|
||||||
this.replyEditor.focus();
|
this.replyEditor.render();
|
||||||
} else if (!isReplying && replyContainer) {
|
this.replyEditor.focus();
|
||||||
// 隐藏回复编辑器
|
} else if (!isReplying && replyContainer) {
|
||||||
replyContainer.innerHTML = '';
|
// 隐藏回复编辑器
|
||||||
this.replyEditor = null;
|
replyContainer.innerHTML = '';
|
||||||
}
|
this.replyEditor = null;
|
||||||
} else if (isReplying && this.replyEditor) {
|
}
|
||||||
// 更新回复编辑器的 props
|
} else if (isReplying && this.replyEditor) {
|
||||||
this.replyEditor.setProps({
|
// 更新回复编辑器的 props
|
||||||
content: this.props.replyContent,
|
this.replyEditor.setProps({
|
||||||
error: this.props.replyError,
|
content: this.props.replyContent,
|
||||||
submitting: this.props.submitting,
|
error: this.props.replyError,
|
||||||
currentUser: this.props.currentUser
|
submitting: this.props.submitting,
|
||||||
});
|
currentUser: this.props.currentUser,
|
||||||
}
|
placeholder: this.props.replyPlaceholder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 递归更新嵌套回复
|
// 递归更新嵌套回复
|
||||||
if (this.childCommentItems && this.childCommentItems.length > 0) {
|
if (this.childCommentItems && this.childCommentItems.length > 0) {
|
||||||
this.childCommentItems.forEach((childItem) => {
|
this.childCommentItems.forEach((childItem) => {
|
||||||
childItem.setProps({
|
childItem.setProps({
|
||||||
replyingTo: this.props.replyingTo,
|
replyingTo: this.props.replyingTo,
|
||||||
replyContent: this.props.replyContent,
|
replyContent: this.props.replyContent,
|
||||||
replyError: this.props.replyError,
|
replyError: this.props.replyError,
|
||||||
submitting: this.props.submitting,
|
submitting: this.props.submitting,
|
||||||
currentUser: this.props.currentUser,
|
currentUser: this.props.currentUser,
|
||||||
enableCommentLike: this.props.enableCommentLike,
|
enableCommentLike: this.props.enableCommentLike,
|
||||||
onLikeComment: this.props.onLikeComment
|
replyPlaceholder: this.props.replyPlaceholder,
|
||||||
});
|
onLikeComment: this.props.onLikeComment,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleReply() {
|
handleReply() {
|
||||||
if (this.props.onReply) {
|
if (this.props.onReply) {
|
||||||
this.props.onReply(this.props.comment.id);
|
this.props.onReply(this.props.comment.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLikeComment() {
|
handleLikeComment() {
|
||||||
if (!this.props.onLikeComment) {
|
if (!this.props.onLikeComment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentId = String(this.props.comment.id);
|
const commentId = String(this.props.comment.id);
|
||||||
|
|
||||||
// 防抖检查:1 秒内同一评论只能操作一次
|
// 防抖检查:1 秒内同一评论只能操作一次
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const debounceKey = `${this.getUserId()}_${commentId}`;
|
const debounceKey = `${this.getUserId()}_${commentId}`;
|
||||||
const lastClick = CommentItem._likeDebounce.get(debounceKey);
|
const lastClick = CommentItem._likeDebounce.get(debounceKey);
|
||||||
if (lastClick && now - lastClick < 1000) {
|
if (lastClick && now - lastClick < 1000) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CommentItem._likeDebounce.set(debounceKey, now);
|
CommentItem._likeDebounce.set(debounceKey, now);
|
||||||
|
|
||||||
// 获取当前点赞状态
|
// 获取当前点赞状态
|
||||||
const likedComments = this.getLikedComments();
|
const likedComments = this.getLikedComments();
|
||||||
const hasLiked = likedComments.has(commentId);
|
const hasLiked = likedComments.has(commentId);
|
||||||
|
|
||||||
if (!hasLiked) {
|
if (!hasLiked) {
|
||||||
// 未点赞,执行点赞
|
// 未点赞,执行点赞
|
||||||
likedComments.add(commentId);
|
likedComments.add(commentId);
|
||||||
this.saveLikedComments(likedComments);
|
this.saveLikedComments(likedComments);
|
||||||
this.props.onLikeComment(commentId, true);
|
this.props.onLikeComment(commentId, true);
|
||||||
}
|
}
|
||||||
// 已点赞则不做任何操作
|
// 已点赞则不做任何操作
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户唯一标识
|
* 获取用户唯一标识
|
||||||
* 使用静态缓存确保一致性
|
* 使用静态缓存确保一致性
|
||||||
* @returns {string} 用户ID
|
* @returns {string} 用户ID
|
||||||
*/
|
*/
|
||||||
getUserId() {
|
getUserId() {
|
||||||
if (CommentItem._userId) {
|
if (CommentItem._userId) {
|
||||||
return CommentItem._userId;
|
return CommentItem._userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'cwd_comment_user_id';
|
const STORAGE_KEY = 'cwd_comment_user_id';
|
||||||
let userId = localStorage.getItem(STORAGE_KEY);
|
let userId = localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
// 生成简单的用户ID
|
// 生成简单的用户ID
|
||||||
userId = 'u_' + Date.now() + '_' + Math.random().toString(36).substring(2, 12);
|
userId = 'u_' + Date.now() + '_' + Math.random().toString(36).substring(2, 12);
|
||||||
localStorage.setItem(STORAGE_KEY, userId);
|
localStorage.setItem(STORAGE_KEY, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
CommentItem._userId = userId;
|
CommentItem._userId = userId;
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取已点赞的评论ID集合
|
* 获取已点赞的评论ID集合
|
||||||
* @returns {Set<string>} 已点赞的评论ID集合
|
* @returns {Set<string>} 已点赞的评论ID集合
|
||||||
*/
|
*/
|
||||||
getLikedComments() {
|
getLikedComments() {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
const key = `cwd_comment_liked_${userId}`;
|
const key = `cwd_comment_liked_${userId}`;
|
||||||
const data = localStorage.getItem(key);
|
const data = localStorage.getItem(key);
|
||||||
const likedSet = new Set();
|
const likedSet = new Set();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
parsed.forEach(id => likedSet.add(String(id)));
|
parsed.forEach((id) => likedSet.add(String(id)));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 解析失败,返回空集合
|
// 解析失败,返回空集合
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return likedSet;
|
return likedSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存点赞记录到 localStorage
|
* 保存点赞记录到 localStorage
|
||||||
* @param {Set} likedSet - 点赞集合
|
* @param {Set} likedSet - 点赞集合
|
||||||
*/
|
*/
|
||||||
saveLikedComments(likedSet) {
|
saveLikedComments(likedSet) {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
const key = `cwd_comment_liked_${userId}`;
|
const key = `cwd_comment_liked_${userId}`;
|
||||||
localStorage.setItem(key, JSON.stringify(Array.from(likedSet)));
|
localStorage.setItem(key, JSON.stringify(Array.from(likedSet)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否已点赞
|
* 检查是否已点赞
|
||||||
* @param {string|number} commentId - 评论 ID
|
* @param {string|number} commentId - 评论 ID
|
||||||
* @returns {boolean} 是否已点赞
|
* @returns {boolean} 是否已点赞
|
||||||
*/
|
*/
|
||||||
hasLiked(commentId) {
|
hasLiked(commentId) {
|
||||||
const likedComments = this.getLikedComments();
|
const likedComments = this.getLikedComments();
|
||||||
return likedComments.has(String(commentId));
|
return likedComments.has(String(commentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmitReply() {
|
handleSubmitReply() {
|
||||||
if (this.props.onSubmitReply) {
|
if (this.props.onSubmitReply) {
|
||||||
this.props.onSubmitReply(this.props.comment.id);
|
this.props.onSubmitReply(this.props.comment.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelReply() {
|
handleCancelReply() {
|
||||||
if (this.props.onCancelReply) {
|
if (this.props.onCancelReply) {
|
||||||
this.props.onCancelReply();
|
this.props.onCancelReply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateReplyContent(content) {
|
handleUpdateReplyContent(content) {
|
||||||
if (this.props.onUpdateReplyContent) {
|
if (this.props.onUpdateReplyContent) {
|
||||||
this.props.onUpdateReplyContent(content);
|
this.props.onUpdateReplyContent(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearReplyError() {
|
handleClearReplyError() {
|
||||||
if (this.props.onClearReplyError) {
|
if (this.props.onClearReplyError) {
|
||||||
this.props.onClearReplyError();
|
this.props.onClearReplyError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export class CommentList extends Component {
|
|||||||
// adminEmail 已移除,改用 comment.isAdmin 字段
|
// adminEmail 已移除,改用 comment.isAdmin 字段
|
||||||
adminBadge: this.props.adminBadge,
|
adminBadge: this.props.adminBadge,
|
||||||
enableCommentLike: this.props.enableCommentLike,
|
enableCommentLike: this.props.enableCommentLike,
|
||||||
|
replyPlaceholder: this.props.replyPlaceholder,
|
||||||
onReply: (commentId) => this.handleReply(commentId),
|
onReply: (commentId) => this.handleReply(commentId),
|
||||||
onSubmitReply: (commentId) => this.handleSubmitReply(commentId),
|
onSubmitReply: (commentId) => this.handleSubmitReply(commentId),
|
||||||
onCancelReply: () => this.handleCancelReply(),
|
onCancelReply: () => this.handleCancelReply(),
|
||||||
|
|||||||
@@ -6,306 +6,305 @@ import { Component } from './Component.js';
|
|||||||
import { renderMarkdown } from '../utils/markdown.js';
|
import { renderMarkdown } from '../utils/markdown.js';
|
||||||
|
|
||||||
export class ReplyEditor extends Component {
|
export class ReplyEditor extends Component {
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement|string} container - 容器元素或选择器
|
* @param {HTMLElement|string} container - 容器元素或选择器
|
||||||
* @param {Object} props - 组件属性
|
* @param {Object} props - 组件属性
|
||||||
* @param {string} props.replyToAuthor - 被回复的作者名
|
* @param {string} props.replyToAuthor - 被回复的作者名
|
||||||
* @param {string} props.content - 回复内容
|
* @param {string} props.content - 回复内容
|
||||||
* @param {string|null} props.error - 错误信息
|
* @param {string|null} props.error - 错误信息
|
||||||
* @param {boolean} props.submitting - 是否正在提交
|
* @param {boolean} props.submitting - 是否正在提交
|
||||||
* @param {Function} props.onUpdate - 内容更新回调
|
* @param {Function} props.onUpdate - 内容更新回调
|
||||||
* @param {Function} props.onSubmit - 提交回调
|
* @param {Function} props.onSubmit - 提交回调
|
||||||
* @param {Function} props.onCancel - 取消回调
|
* @param {Function} props.onCancel - 取消回调
|
||||||
* @param {Function} props.onClearError - 清除错误回调
|
* @param {Function} props.onClearError - 清除错误回调
|
||||||
*/
|
*/
|
||||||
constructor(container, props = {}) {
|
constructor(container, props = {}) {
|
||||||
super(container, props);
|
super(container, props);
|
||||||
const { currentUser } = props;
|
const { currentUser } = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
content: props.content || '',
|
content: props.content || '',
|
||||||
// 如果没有昵称或邮箱,显示用户信息输入框。且一旦显示,在当前编辑器生命周期内保持显示,避免输入过程中消失
|
// 如果没有昵称或邮箱,显示用户信息输入框。且一旦显示,在当前编辑器生命周期内保持显示,避免输入过程中消失
|
||||||
showUserInfo: !currentUser || !currentUser.name || !currentUser.email,
|
showUserInfo: !currentUser || !currentUser.name || !currentUser.email,
|
||||||
showPreview: false,
|
showPreview: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentUser } = this.props;
|
const { currentUser } = this.props;
|
||||||
const { showUserInfo } = this.state;
|
const { showUserInfo } = this.state;
|
||||||
|
const placeholderText = this.props.placeholder || '';
|
||||||
|
|
||||||
const root = this.createElement('div', {
|
const root = this.createElement('div', {
|
||||||
className: 'cwd-reply-editor',
|
className: 'cwd-reply-editor',
|
||||||
children: [
|
children: [
|
||||||
// 头部
|
// 头部
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-reply-header',
|
className: 'cwd-reply-header',
|
||||||
children: [
|
children: [
|
||||||
this.createTextElement('span', `回复 @${this.props.replyToAuthor}`, 'cwd-reply-to'),
|
this.createTextElement('span', `回复 @${this.props.replyToAuthor}`, 'cwd-reply-to'),
|
||||||
this.createElement('button', {
|
this.createElement('button', {
|
||||||
className: 'cwd-btn-close',
|
className: 'cwd-btn-close',
|
||||||
attributes: {
|
attributes: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onClick: () => this.handleCancel()
|
onClick: () => this.handleCancel(),
|
||||||
},
|
},
|
||||||
text: '✕'
|
text: '✕',
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 用户信息输入框(当缺少信息时显示)
|
// 用户信息输入框(当缺少信息时显示)
|
||||||
...(showUserInfo ? [
|
...(showUserInfo
|
||||||
this.createElement('div', {
|
? [
|
||||||
className: 'cwd-form-row',
|
this.createElement('div', {
|
||||||
attributes: {
|
className: 'cwd-form-row',
|
||||||
style: 'margin-bottom: 12px;'
|
attributes: {
|
||||||
},
|
style: 'margin-bottom: 12px;',
|
||||||
children: [
|
},
|
||||||
this.createFormField('昵称 *', 'text', 'name', currentUser?.name),
|
children: [
|
||||||
this.createFormField('邮箱 *', 'email', 'email', currentUser?.email),
|
this.createFormField('昵称 *', 'text', 'name', currentUser?.name),
|
||||||
this.createFormField('网址', 'url', 'url', currentUser?.url)
|
this.createFormField('邮箱 *', 'email', 'email', currentUser?.email),
|
||||||
]
|
this.createFormField('网址', 'url', 'url', currentUser?.url),
|
||||||
})
|
],
|
||||||
] : []),
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
// 文本框
|
// 文本框
|
||||||
this.createElement('textarea', {
|
this.createElement('textarea', {
|
||||||
className: 'cwd-reply-textarea',
|
className: 'cwd-reply-textarea',
|
||||||
attributes: {
|
attributes: {
|
||||||
rows: 3,
|
rows: 3,
|
||||||
placeholder: '支持 Markdown 格式',
|
placeholder: placeholderText,
|
||||||
disabled: this.props.submitting,
|
disabled: this.props.submitting,
|
||||||
onInput: (e) => this.handleInput(e),
|
onInput: (e) => this.handleInput(e),
|
||||||
onKeydown: (e) => this.handleTextareaKeydown(e),
|
onKeydown: (e) => this.handleTextareaKeydown(e),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 错误提示
|
// 错误提示
|
||||||
...(this.props.error ? [
|
...(this.props.error
|
||||||
this.createElement('div', {
|
? [
|
||||||
className: 'cwd-error-inline cwd-error-small',
|
this.createElement('div', {
|
||||||
children: [
|
className: 'cwd-error-inline cwd-error-small',
|
||||||
this.createTextElement('span', this.props.error),
|
children: [
|
||||||
this.createElement('button', {
|
this.createTextElement('span', this.props.error),
|
||||||
className: 'cwd-error-close',
|
this.createElement('button', {
|
||||||
attributes: {
|
className: 'cwd-error-close',
|
||||||
type: 'button',
|
attributes: {
|
||||||
onClick: () => this.handleClearError()
|
type: 'button',
|
||||||
},
|
onClick: () => this.handleClearError(),
|
||||||
text: '✕'
|
},
|
||||||
})
|
text: '✕',
|
||||||
]
|
}),
|
||||||
})
|
],
|
||||||
] : []),
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-reply-actions',
|
className: 'cwd-reply-actions',
|
||||||
children: [
|
children: [
|
||||||
this.createElement('button', {
|
this.createElement('button', {
|
||||||
className: `cwd-btn cwd-btn-secondary cwd-btn-small cwd-btn-preview ${this.state.showPreview ? 'cwd-btn-active' : ''}`,
|
className: `cwd-btn cwd-btn-secondary cwd-btn-small cwd-btn-preview ${this.state.showPreview ? 'cwd-btn-active' : ''}`,
|
||||||
attributes: {
|
attributes: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled: this.props.submitting || !this.state.content.trim(),
|
disabled: this.props.submitting || !this.state.content.trim(),
|
||||||
onClick: () => this.togglePreview(),
|
onClick: () => this.togglePreview(),
|
||||||
},
|
},
|
||||||
text: this.state.showPreview ? '关闭' : '预览',
|
text: this.state.showPreview ? '关闭' : '预览',
|
||||||
}),
|
}),
|
||||||
this.createElement('button', {
|
this.createElement('button', {
|
||||||
className: 'cwd-btn cwd-btn-primary cwd-btn-small',
|
className: 'cwd-btn cwd-btn-primary cwd-btn-small',
|
||||||
attributes: {
|
attributes: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled: this.props.submitting || !this.state.content.trim(),
|
disabled: this.props.submitting || !this.state.content.trim(),
|
||||||
onClick: () => this.handleSubmit()
|
onClick: () => this.handleSubmit(),
|
||||||
},
|
},
|
||||||
text: this.props.submitting ? '提交中...' : '提交回复'
|
text: this.props.submitting ? '提交中...' : '提交回复',
|
||||||
}),
|
}),
|
||||||
this.createElement('button', {
|
this.createElement('button', {
|
||||||
className: 'cwd-btn cwd-btn-secondary cwd-btn-small',
|
className: 'cwd-btn cwd-btn-secondary cwd-btn-small',
|
||||||
attributes: {
|
attributes: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled: this.props.submitting,
|
disabled: this.props.submitting,
|
||||||
onClick: () => this.handleCancel()
|
onClick: () => this.handleCancel(),
|
||||||
},
|
},
|
||||||
text: '取消'
|
text: '取消',
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 预览区域
|
// 预览区域
|
||||||
...(this.state.showPreview && this.state.content
|
...(this.state.showPreview && this.state.content
|
||||||
? [
|
? [
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-preview-container',
|
className: 'cwd-preview-container',
|
||||||
children: [
|
children: [
|
||||||
this.createElement('div', {
|
this.createElement('div', {
|
||||||
className: 'cwd-preview-content cwd-comment-content',
|
className: 'cwd-preview-content cwd-comment-content',
|
||||||
// 直接设置 innerHTML
|
// 直接设置 innerHTML
|
||||||
html: renderMarkdown(this.state.content),
|
html: renderMarkdown(this.state.content),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置文本框内容
|
// 设置文本框内容
|
||||||
const textarea = root.querySelector('textarea');
|
const textarea = root.querySelector('textarea');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.value = this.state.content;
|
textarea.value = this.state.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.elements.root = root;
|
this.elements.root = root;
|
||||||
this.empty(this.container);
|
this.empty(this.container);
|
||||||
this.container.appendChild(root);
|
this.container.appendChild(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(prevProps) {
|
updateProps(prevProps) {
|
||||||
// 如果外部传入的 content 变化,更新内部状态
|
// 如果外部传入的 content 变化,更新内部状态
|
||||||
if (this.props.content !== this.state.content && this.props.content !== prevProps?.content) {
|
if (this.props.content !== this.state.content && this.props.content !== prevProps?.content) {
|
||||||
this.state.content = this.props.content;
|
this.state.content = this.props.content;
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果用户信息变化,重新渲染
|
// 如果用户信息变化,重新渲染
|
||||||
if (JSON.stringify(this.props.currentUser) !== JSON.stringify(prevProps?.currentUser)) {
|
if (JSON.stringify(this.props.currentUser) !== JSON.stringify(prevProps?.currentUser)) {
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有错误显示/隐藏变化,重新渲染
|
// 如果有错误显示/隐藏变化,重新渲染
|
||||||
if (this.props.error !== prevProps?.error) {
|
if (this.props.error !== prevProps?.error) {
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 submitting 状态变化,重新渲染
|
// 如果 submitting 状态变化,重新渲染
|
||||||
if (this.props.submitting !== prevProps?.submitting) {
|
if (this.props.submitting !== prevProps?.submitting) {
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTextareaKeydown(e) {
|
handleTextareaKeydown(e) {
|
||||||
if (
|
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
||||||
e.key === '/' &&
|
e.stopPropagation();
|
||||||
!e.ctrlKey &&
|
}
|
||||||
!e.metaKey &&
|
}
|
||||||
!e.altKey &&
|
|
||||||
!e.shiftKey
|
|
||||||
) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePreview() {
|
togglePreview() {
|
||||||
this.state.showPreview = !this.state.showPreview;
|
this.state.showPreview = !this.state.showPreview;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(e) {
|
handleInput(e) {
|
||||||
this.state.content = e.target.value;
|
this.state.content = e.target.value;
|
||||||
|
|
||||||
// 更新提交按钮的禁用状态
|
|
||||||
const submitBtn = this.elements.root?.querySelector('.cwd-btn-primary');
|
|
||||||
if (submitBtn) {
|
|
||||||
submitBtn.disabled = this.props.submitting || !this.state.content.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新预览按钮的禁用状态
|
// 更新提交按钮的禁用状态
|
||||||
const previewBtn = this.elements.root?.querySelector('.cwd-btn-preview');
|
const submitBtn = this.elements.root?.querySelector('.cwd-btn-primary');
|
||||||
if (previewBtn) {
|
if (submitBtn) {
|
||||||
previewBtn.disabled = this.props.submitting || !this.state.content.trim();
|
submitBtn.disabled = this.props.submitting || !this.state.content.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onUpdate) {
|
// 更新预览按钮的禁用状态
|
||||||
this.props.onUpdate(this.state.content);
|
const previewBtn = this.elements.root?.querySelector('.cwd-btn-preview');
|
||||||
}
|
if (previewBtn) {
|
||||||
|
previewBtn.disabled = this.props.submitting || !this.state.content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
// 实时更新预览内容
|
if (this.props.onUpdate) {
|
||||||
if (this.state.showPreview) {
|
this.props.onUpdate(this.state.content);
|
||||||
this.updatePreviewContent(this.state.content);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePreviewContent(content) {
|
// 实时更新预览内容
|
||||||
const previewContent = this.elements.root?.querySelector('.cwd-preview-content');
|
if (this.state.showPreview) {
|
||||||
if (previewContent) {
|
this.updatePreviewContent(this.state.content);
|
||||||
previewContent.innerHTML = renderMarkdown(content);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit() {
|
updatePreviewContent(content) {
|
||||||
if (this.props.onSubmit) {
|
const previewContent = this.elements.root?.querySelector('.cwd-preview-content');
|
||||||
this.props.onSubmit();
|
if (previewContent) {
|
||||||
}
|
previewContent.innerHTML = renderMarkdown(content);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleCancel() {
|
handleSubmit() {
|
||||||
if (this.props.onCancel) {
|
if (this.props.onSubmit) {
|
||||||
this.props.onCancel();
|
this.props.onSubmit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearError() {
|
handleCancel() {
|
||||||
if (this.props.onClearError) {
|
if (this.props.onCancel) {
|
||||||
this.props.onClearError();
|
this.props.onCancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
handleClearError() {
|
||||||
* 设置内容
|
if (this.props.onClearError) {
|
||||||
* @param {string} content - 新内容
|
this.props.onClearError();
|
||||||
*/
|
}
|
||||||
setContent(content) {
|
}
|
||||||
this.state.content = content;
|
|
||||||
const textarea = this.elements.root?.querySelector('textarea');
|
|
||||||
if (textarea) {
|
|
||||||
textarea.value = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取内容
|
* 设置内容
|
||||||
* @returns {string}
|
* @param {string} content - 新内容
|
||||||
*/
|
*/
|
||||||
getContent() {
|
setContent(content) {
|
||||||
return this.state.content;
|
this.state.content = content;
|
||||||
}
|
const textarea = this.elements.root?.querySelector('textarea');
|
||||||
|
if (textarea) {
|
||||||
|
textarea.value = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 聚焦文本框
|
* 获取内容
|
||||||
*/
|
* @returns {string}
|
||||||
focus() {
|
*/
|
||||||
const textarea = this.elements.root?.querySelector('textarea');
|
getContent() {
|
||||||
if (textarea) {
|
return this.state.content;
|
||||||
textarea.focus();
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUserInfoChange(field, value) {
|
/**
|
||||||
if (this.props.onUpdateUserInfo) {
|
* 聚焦文本框
|
||||||
this.props.onUpdateUserInfo(field, value);
|
*/
|
||||||
}
|
focus() {
|
||||||
}
|
const textarea = this.elements.root?.querySelector('textarea');
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createFormField(placeholder, type, field, value) {
|
handleUserInfoChange(field, value) {
|
||||||
return this.createElement('div', {
|
if (this.props.onUpdateUserInfo) {
|
||||||
className: 'cwd-form-field',
|
this.props.onUpdateUserInfo(field, value);
|
||||||
children: [
|
}
|
||||||
this.createElement('input', {
|
}
|
||||||
className: 'cwd-form-input',
|
|
||||||
attributes: {
|
createFormField(placeholder, type, field, value) {
|
||||||
type,
|
return this.createElement('div', {
|
||||||
placeholder,
|
className: 'cwd-form-field',
|
||||||
value: value || '',
|
children: [
|
||||||
disabled: this.props.submitting,
|
this.createElement('input', {
|
||||||
onInput: (e) => this.handleUserInfoChange(field, e.target.value),
|
className: 'cwd-form-input',
|
||||||
onKeydown: (e) => this.handleTextareaKeydown(e)
|
attributes: {
|
||||||
}
|
type,
|
||||||
})
|
placeholder,
|
||||||
]
|
value: value || '',
|
||||||
});
|
disabled: this.props.submitting,
|
||||||
}
|
onInput: (e) => this.handleUserInfoChange(field, e.target.value),
|
||||||
|
onKeydown: (e) => this.handleTextareaKeydown(e),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export class CWDComments {
|
|||||||
allowedDomains: Array.isArray(data.allowedDomains) ? data.allowedDomains : [],
|
allowedDomains: Array.isArray(data.allowedDomains) ? data.allowedDomains : [],
|
||||||
enableCommentLike: typeof data.enableCommentLike === 'boolean' ? data.enableCommentLike : true,
|
enableCommentLike: typeof data.enableCommentLike === 'boolean' ? data.enableCommentLike : true,
|
||||||
enableArticleLike: typeof data.enableArticleLike === 'boolean' ? data.enableArticleLike : true,
|
enableArticleLike: typeof data.enableArticleLike === 'boolean' ? data.enableArticleLike : true,
|
||||||
|
commentPlaceholder:
|
||||||
|
typeof data.commentPlaceholder === 'string' ? data.commentPlaceholder : undefined,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {};
|
return {};
|
||||||
@@ -168,16 +170,20 @@ export class CWDComments {
|
|||||||
this.config.requireReview = !!serverConfig.requireReview;
|
this.config.requireReview = !!serverConfig.requireReview;
|
||||||
this.config.enableCommentLike = serverConfig.enableCommentLike;
|
this.config.enableCommentLike = serverConfig.enableCommentLike;
|
||||||
this.config.enableArticleLike = serverConfig.enableArticleLike;
|
this.config.enableArticleLike = serverConfig.enableArticleLike;
|
||||||
|
this.config.commentPlaceholder =
|
||||||
|
typeof serverConfig.commentPlaceholder === 'string'
|
||||||
|
? serverConfig.commentPlaceholder
|
||||||
|
: this.config.commentPlaceholder;
|
||||||
|
|
||||||
const api = createApiClient(this.config);
|
const api = createApiClient(this.config);
|
||||||
this.api = api;
|
this.api = api;
|
||||||
if (this.hostElement && this.mountPoint) {
|
if (this.hostElement && this.mountPoint) {
|
||||||
this.store = createCommentStore(
|
this.store = createCommentStore(
|
||||||
this.config,
|
this.config,
|
||||||
api.fetchComments.bind(api),
|
api.fetchComments.bind(api),
|
||||||
api.submitComment.bind(api),
|
api.submitComment.bind(api),
|
||||||
typeof api.likeComment === 'function' ? api.likeComment.bind(api) : undefined,
|
typeof api.likeComment === 'function' ? api.likeComment.bind(api) : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.unsubscribe = this.store.store.subscribe((state, prevState) => {
|
this.unsubscribe = this.store.store.subscribe((state, prevState) => {
|
||||||
this._onStateChange(state, prevState);
|
this._onStateChange(state, prevState);
|
||||||
@@ -339,6 +345,7 @@ export class CWDComments {
|
|||||||
onFieldChange: (field, value) => this.store.updateFormField(field, value),
|
onFieldChange: (field, value) => this.store.updateFormField(field, value),
|
||||||
adminEmail: this.config.adminEmail,
|
adminEmail: this.config.adminEmail,
|
||||||
onVerifyAdmin: (key) => this.api.verifyAdminKey(key),
|
onVerifyAdmin: (key) => this.api.verifyAdminKey(key),
|
||||||
|
placeholder: this.config.commentPlaceholder,
|
||||||
});
|
});
|
||||||
this.commentForm.render();
|
this.commentForm.render();
|
||||||
}
|
}
|
||||||
@@ -367,7 +374,7 @@ export class CWDComments {
|
|||||||
const listContainer = document.createElement('div');
|
const listContainer = document.createElement('div');
|
||||||
this.mountPoint.appendChild(listContainer);
|
this.mountPoint.appendChild(listContainer);
|
||||||
|
|
||||||
this.commentList = new CommentList(listContainer, {
|
this.commentList = new CommentList(listContainer, {
|
||||||
comments: state.comments,
|
comments: state.comments,
|
||||||
loading: state.loading,
|
loading: state.loading,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -388,6 +395,7 @@ export class CWDComments {
|
|||||||
onCancelReply: () => this.store.cancelReply(),
|
onCancelReply: () => this.store.cancelReply(),
|
||||||
onUpdateReplyContent: (content) => this.store.updateReplyContent(content),
|
onUpdateReplyContent: (content) => this.store.updateReplyContent(content),
|
||||||
onClearReplyError: () => this.store.clearReplyError(),
|
onClearReplyError: () => this.store.clearReplyError(),
|
||||||
|
replyPlaceholder: this.config.commentPlaceholder,
|
||||||
onPrevPage: () => this.store.goToPage(state.pagination.page - 1),
|
onPrevPage: () => this.store.goToPage(state.pagination.page - 1),
|
||||||
onNextPage: () => this.store.goToPage(state.pagination.page + 1),
|
onNextPage: () => this.store.goToPage(state.pagination.page + 1),
|
||||||
onGoToPage: (page) => this.store.goToPage(page),
|
onGoToPage: (page) => this.store.goToPage(page),
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--cwd-text, #24292f);
|
color: var(--cwd-text, #24292f);
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cwd-form-field input,
|
.cwd-form-field input,
|
||||||
@@ -165,6 +166,11 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cwd-form-field input::placeholder,
|
||||||
|
.cwd-form-field textarea::placeholder {
|
||||||
|
color: var(--cwd-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.cwd-form-field input:focus,
|
.cwd-form-field input:focus,
|
||||||
.cwd-form-field textarea:focus {
|
.cwd-form-field textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -318,9 +324,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 鼠标悬停在评论主体上时显示操作按钮 */
|
/* 鼠标悬停在评论主体上时显示操作按钮 */
|
||||||
.cwd-comment-body:hover .cwd-action-btn {
|
/* .cwd-comment-body:hover>.cwd-comment-header>.cwd-comment-actions>.cwd-action-btn {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.cwd-comment-item:last-child {
|
.cwd-comment-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -681,7 +687,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
display: none;
|
/* display: none; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cwd-action-btn:hover {
|
.cwd-action-btn:hover {
|
||||||
@@ -1023,4 +1029,4 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user