Merge branch 'main' of https://github.com/anghunk/cwd
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -174,4 +174,8 @@ wrangler.jsonc
|
|||||||
.claude
|
.claude
|
||||||
.trae
|
.trae
|
||||||
|
|
||||||
.npmrc
|
.npmrc
|
||||||
|
|
||||||
|
# idea
|
||||||
|
|
||||||
|
.idea
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Context } from 'hono'
|
import { Context } from 'hono'
|
||||||
import { Bindings } from '../../bindings'
|
import { Bindings } from '../../bindings'
|
||||||
import { getCravatar } from '../../utils/getAvatar'
|
import { getCravatar } from '../../utils/getAvatar'
|
||||||
|
import { decodePostSlug, getAllSlugFormats, escapeLikePattern } from '../../utils/decodePostSlug'
|
||||||
|
|
||||||
export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||||
const rawPostSlug = c.req.query('post_slug') || ''
|
const rawPostSlug = c.req.query('post_slug') || ''
|
||||||
const postSlug = rawPostSlug.trim()
|
const postSlug = decodePostSlug(rawPostSlug)
|
||||||
const page = parseInt(c.req.query('page') || '1')
|
const page = parseInt(c.req.query('page') || '1')
|
||||||
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
|
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
|
||||||
const nested = c.req.query('nested') !== 'false'
|
const nested = c.req.query('nested') !== 'false'
|
||||||
@@ -42,13 +43,14 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const equalSlugs = Array.from(new Set(slugList))
|
const allSlugFormats = new Set<string>()
|
||||||
const likePatternsSet = new Set<string>()
|
for (const s of slugList) {
|
||||||
for (const s of equalSlugs) {
|
for (const format of getAllSlugFormats(s)) {
|
||||||
likePatternsSet.add(`${s}#%`)
|
allSlugFormats.add(format)
|
||||||
likePatternsSet.add(`${s}?%`)
|
}
|
||||||
}
|
}
|
||||||
const likePatterns = Array.from(likePatternsSet)
|
const equalSlugs = Array.from(allSlugFormats)
|
||||||
|
|
||||||
const whereParts: string[] = []
|
const whereParts: string[] = []
|
||||||
if (equalSlugs.length === 1) {
|
if (equalSlugs.length === 1) {
|
||||||
whereParts.push('post_slug = ?')
|
whereParts.push('post_slug = ?')
|
||||||
@@ -56,16 +58,13 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const placeholders = equalSlugs.map(() => '?').join(', ')
|
const placeholders = equalSlugs.map(() => '?').join(', ')
|
||||||
whereParts.push(`post_slug IN (${placeholders})`)
|
whereParts.push(`post_slug IN (${placeholders})`)
|
||||||
}
|
}
|
||||||
for (let i = 0; i < likePatterns.length; i += 1) {
|
|
||||||
whereParts.push('post_slug LIKE ?')
|
|
||||||
}
|
|
||||||
const whereClause =
|
const whereClause =
|
||||||
whereParts.length > 0
|
whereParts.length > 0
|
||||||
? `status = "approved" AND (${whereParts.join(' OR ')})`
|
? `(${whereParts.join(' OR ')})`
|
||||||
: 'status = "approved"'
|
: '1=1'
|
||||||
|
|
||||||
let finalWhereClause = whereClause
|
let finalWhereClause = `status = "approved" AND ${whereClause}`
|
||||||
const bindParams: unknown[] = [...equalSlugs, ...likePatterns]
|
const bindParams: unknown[] = [...equalSlugs]
|
||||||
|
|
||||||
if (siteId) {
|
if (siteId) {
|
||||||
finalWhereClause += ' AND site_id = ?'
|
finalWhereClause += ' AND site_id = ?'
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
import type { Bindings } from '../../bindings';
|
import type { Bindings } from '../../bindings';
|
||||||
|
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||||
|
|
||||||
export const getPagePv = async (c: Context<{ Bindings: Bindings }>) => {
|
export const getPagePv = async (c: Context<{ Bindings: Bindings }>) => {
|
||||||
try {
|
try {
|
||||||
const rawPostSlug = c.req.query('post_slug') || '';
|
const rawPostSlug = c.req.query('post_slug') || '';
|
||||||
const postSlug = rawPostSlug.trim();
|
const postSlug = decodePostSlug(rawPostSlug);
|
||||||
const rawSiteId = c.req.query('siteId') || '';
|
const rawSiteId = c.req.query('siteId') || '';
|
||||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : '';
|
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
import type { Bindings } from '../../bindings';
|
import type { Bindings } from '../../bindings';
|
||||||
|
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||||
|
|
||||||
type LikeStatusResponse = {
|
type LikeStatusResponse = {
|
||||||
liked: boolean;
|
liked: boolean;
|
||||||
@@ -36,7 +37,7 @@ export const getLikeStatus = async (
|
|||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const rawPostSlug = c.req.query('post_slug') || '';
|
const rawPostSlug = c.req.query('post_slug') || '';
|
||||||
const postSlug = rawPostSlug.trim();
|
const postSlug = decodePostSlug(rawPostSlug);
|
||||||
const siteId = c.req.query('siteId') || '';
|
const siteId = c.req.query('siteId') || '';
|
||||||
|
|
||||||
if (!postSlug) {
|
if (!postSlug) {
|
||||||
@@ -92,13 +93,14 @@ export const likePage = async (
|
|||||||
|
|
||||||
const rawPostSlug =
|
const rawPostSlug =
|
||||||
typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||||
|
const postSlug = decodePostSlug(rawPostSlug);
|
||||||
const rawPostTitle =
|
const rawPostTitle =
|
||||||
typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||||
const rawPostUrl =
|
const rawPostUrl =
|
||||||
typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||||
const siteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
const siteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||||
|
|
||||||
if (!rawPostSlug) {
|
if (!postSlug) {
|
||||||
return c.json({ message: 'postSlug is required' }, 400);
|
return c.json({ message: 'postSlug is required' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ export const likePage = async (
|
|||||||
const existingLike = await c.env.CWD_DB.prepare(
|
const existingLike = await c.env.CWD_DB.prepare(
|
||||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?'
|
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?'
|
||||||
)
|
)
|
||||||
.bind(rawPostSlug, userId, siteId)
|
.bind(postSlug, userId, siteId)
|
||||||
.first<{ id: number }>();
|
.first<{ id: number }>();
|
||||||
|
|
||||||
let alreadyLiked = false;
|
let alreadyLiked = false;
|
||||||
@@ -118,7 +120,7 @@ export const likePage = async (
|
|||||||
await c.env.CWD_DB.prepare(
|
await c.env.CWD_DB.prepare(
|
||||||
'INSERT INTO Likes (page_slug, user_id, created_at, site_id) VALUES (?, ?, ?, ?)'
|
'INSERT INTO Likes (page_slug, user_id, created_at, site_id) VALUES (?, ?, ?, ?)'
|
||||||
)
|
)
|
||||||
.bind(rawPostSlug, userId, now, siteId)
|
.bind(postSlug, userId, now, siteId)
|
||||||
.run();
|
.run();
|
||||||
} else {
|
} else {
|
||||||
alreadyLiked = true;
|
alreadyLiked = true;
|
||||||
@@ -127,7 +129,7 @@ export const likePage = async (
|
|||||||
const pageStatsRow = await c.env.CWD_DB.prepare(
|
const pageStatsRow = await c.env.CWD_DB.prepare(
|
||||||
'SELECT id FROM page_stats WHERE post_slug = ? AND site_id = ?'
|
'SELECT id FROM page_stats WHERE post_slug = ? AND site_id = ?'
|
||||||
)
|
)
|
||||||
.bind(rawPostSlug, siteId)
|
.bind(postSlug, siteId)
|
||||||
.first<{ id: number }>();
|
.first<{ id: number }>();
|
||||||
|
|
||||||
if (!pageStatsRow) {
|
if (!pageStatsRow) {
|
||||||
@@ -135,7 +137,7 @@ export const likePage = async (
|
|||||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at, site_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at, site_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
rawPostSlug,
|
postSlug,
|
||||||
rawPostTitle || null,
|
rawPostTitle || null,
|
||||||
rawPostUrl || null,
|
rawPostUrl || null,
|
||||||
0,
|
0,
|
||||||
@@ -161,7 +163,7 @@ export const likePage = async (
|
|||||||
const totalRow = await c.env.CWD_DB.prepare(
|
const totalRow = await c.env.CWD_DB.prepare(
|
||||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?'
|
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?'
|
||||||
)
|
)
|
||||||
.bind(rawPostSlug, siteId)
|
.bind(postSlug, siteId)
|
||||||
.first<{ count: number }>();
|
.first<{ count: number }>();
|
||||||
|
|
||||||
const totalLikes = totalRow?.count || 0;
|
const totalLikes = totalRow?.count || 0;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const likeComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = parsed;
|
const id = parsed;
|
||||||
|
const method = c.req.method;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = await c.env.CWD_DB.prepare(
|
const existing = await c.env.CWD_DB.prepare(
|
||||||
@@ -39,26 +40,20 @@ export const likeComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
return c.json({ message: 'Comment not found' }, 404);
|
return c.json({ message: 'Comment not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delta = method === 'DELETE' ? -1 : 1;
|
||||||
|
const currentLikes = typeof existing.likes === 'number' && Number.isFinite(existing.likes) && existing.likes >= 0
|
||||||
|
? existing.likes
|
||||||
|
: 0;
|
||||||
|
const newLikes = Math.max(0, currentLikes + delta);
|
||||||
|
|
||||||
await c.env.CWD_DB.prepare(
|
await c.env.CWD_DB.prepare(
|
||||||
'UPDATE Comment SET likes = COALESCE(likes, 0) + 1 WHERE id = ?'
|
'UPDATE Comment SET likes = ? WHERE id = ?'
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(newLikes, id)
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const updated = await c.env.CWD_DB.prepare(
|
return c.json({ id, likes: newLikes });
|
||||||
'SELECT COALESCE(likes, 0) as likes FROM Comment WHERE id = ?'
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.first<{ likes?: number }>();
|
|
||||||
|
|
||||||
const likes =
|
|
||||||
updated && typeof updated.likes === 'number' && Number.isFinite(updated.likes) && updated.likes >= 0
|
|
||||||
? updated.likes
|
|
||||||
: ((existing.likes || 0) + 1);
|
|
||||||
|
|
||||||
return c.json({ id, likes });
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return c.json({ message: e?.message || '点赞失败' }, 500);
|
return c.json({ message: e?.message || '操作失败' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
EmailNotificationSettings
|
EmailNotificationSettings
|
||||||
} from '../../utils/email';
|
} from '../../utils/email';
|
||||||
import { loadTelegramSettings, sendTelegramMessage } from '../../utils/telegram';
|
import { loadTelegramSettings, sendTelegramMessage } from '../../utils/telegram';
|
||||||
|
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||||
|
|
||||||
// 检查内容,将<script>标签之间的内容删除
|
|
||||||
export function checkContent(content: string): string {
|
export function checkContent(content: string): string {
|
||||||
return content.replace(/<script[\s\S]*?<\/script>/g, "");
|
return content.replace(/<script[\s\S]*?<\/script>/g, "");
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return c.json({ message: '无效的请求体' }, 400);
|
return c.json({ message: '无效的请求体' }, 400);
|
||||||
}
|
}
|
||||||
const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
|
const { post_slug: rawPostSlug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
|
||||||
|
const post_slug = decodePostSlug(rawPostSlug || '');
|
||||||
const site_id = data.site_id ? String(data.site_id).trim() : "";
|
const site_id = data.site_id ? String(data.site_id).trim() : "";
|
||||||
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
|
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
|
||||||
if (!post_slug || typeof post_slug !== 'string') {
|
if (!post_slug || typeof post_slug !== 'string') {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
import type { Bindings } from '../../bindings';
|
import type { Bindings } from '../../bindings';
|
||||||
|
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||||
|
|
||||||
type TrackVisitBody = {
|
type TrackVisitBody = {
|
||||||
postSlug?: string;
|
postSlug?: string;
|
||||||
@@ -32,11 +33,12 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const body = (await c.req.json().catch(() => ({}))) as TrackVisitBody;
|
const body = (await c.req.json().catch(() => ({}))) as TrackVisitBody;
|
||||||
|
|
||||||
const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||||
|
const postSlug = decodePostSlug(rawPostSlug);
|
||||||
const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||||
const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||||
const rawSiteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
const rawSiteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||||
|
|
||||||
if (!rawPostSlug) {
|
if (!postSlug) {
|
||||||
return c.json({ message: 'postSlug is required' }, 400);
|
return c.json({ message: 'postSlug is required' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +49,9 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
|
|
||||||
const domain =
|
const domain =
|
||||||
extractDomain(rawPostUrl) ||
|
extractDomain(rawPostUrl) ||
|
||||||
extractDomain(rawPostSlug) ||
|
extractDomain(postSlug) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// Upsert page_stats using ON CONFLICT (site_id, post_slug)
|
|
||||||
await c.env.CWD_DB.prepare(
|
await c.env.CWD_DB.prepare(
|
||||||
`INSERT INTO page_stats (site_id, post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at)
|
`INSERT INTO page_stats (site_id, post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
||||||
@@ -63,7 +64,7 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
rawSiteId,
|
rawSiteId,
|
||||||
rawPostSlug,
|
postSlug,
|
||||||
rawPostTitle || null,
|
rawPostTitle || null,
|
||||||
rawPostUrl || null,
|
rawPostUrl || null,
|
||||||
nowTs,
|
nowTs,
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ app.get('/api/analytics/pv', getPagePv);
|
|||||||
app.get('/api/like', getLikeStatus);
|
app.get('/api/like', getLikeStatus);
|
||||||
app.post('/api/like', likePage);
|
app.post('/api/like', likePage);
|
||||||
app.post('/api/comments/like', likeComment);
|
app.post('/api/comments/like', likeComment);
|
||||||
|
app.delete('/api/comments/like', likeComment);
|
||||||
app.post('/api/telegram/webhook', telegramWebhook);
|
app.post('/api/telegram/webhook', telegramWebhook);
|
||||||
app.get('/api/config/comments', async (c) => {
|
app.get('/api/config/comments', async (c) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
37
cwd-api/src/utils/decodePostSlug.ts
Normal file
37
cwd-api/src/utils/decodePostSlug.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export function decodePostSlug(slug: string): string {
|
||||||
|
if (!slug) return slug;
|
||||||
|
let decoded = slug.trim();
|
||||||
|
let prev = '';
|
||||||
|
while (prev !== decoded) {
|
||||||
|
prev = decoded;
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(decoded);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllSlugFormats(slug: string): string[] {
|
||||||
|
if (!slug) return [];
|
||||||
|
const decoded = decodePostSlug(slug);
|
||||||
|
const formats = new Set<string>();
|
||||||
|
|
||||||
|
formats.add(decoded);
|
||||||
|
|
||||||
|
if (decoded !== slug) {
|
||||||
|
formats.add(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoded = encodeURI(decoded);
|
||||||
|
if (encoded !== decoded && !formats.has(encoded)) {
|
||||||
|
formats.add(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeLikePattern(str: string): string {
|
||||||
|
return str.replace(/[%_]/g, '|$&');
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
"marked": "^17.0.1"
|
"marked": "^17.0.1",
|
||||||
|
"node-emoji": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { Component } from './Component.js';
|
import { Component } from './Component.js';
|
||||||
import { ReplyEditor } from './ReplyEditor.js';
|
import { ReplyEditor } from './ReplyEditor.js';
|
||||||
import { formatRelativeTime } from '@/utils/date.js';
|
import { formatRelativeTime } from '@/utils/date.js';
|
||||||
|
import { replaceEmojiInHtml } from '@/utils/markdown.js';
|
||||||
|
|
||||||
export class CommentItem extends Component {
|
export class CommentItem extends Component {
|
||||||
// 防抖缓存,防止连续点击
|
// 防抖缓存,防止连续点击
|
||||||
@@ -202,7 +203,7 @@ export class CommentItem extends Component {
|
|||||||
// 设置评论内容的 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 = replaceEmojiInHtml(comment.contentHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建回复编辑器
|
// 创建回复编辑器
|
||||||
@@ -371,8 +372,12 @@ export class CommentItem extends Component {
|
|||||||
likedComments.add(commentId);
|
likedComments.add(commentId);
|
||||||
this.saveLikedComments(likedComments);
|
this.saveLikedComments(likedComments);
|
||||||
this.props.onLikeComment(commentId, true);
|
this.props.onLikeComment(commentId, true);
|
||||||
|
} else {
|
||||||
|
// 已点赞,执行取消点赞
|
||||||
|
likedComments.delete(commentId);
|
||||||
|
this.saveLikedComments(likedComments);
|
||||||
|
this.props.onLikeComment(commentId, false);
|
||||||
}
|
}
|
||||||
// 已点赞则不做任何操作
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -466,8 +466,14 @@ export class CWDComments {
|
|||||||
onUpdateReplyContent: (content) => this.store.updateReplyContent(content),
|
onUpdateReplyContent: (content) => this.store.updateReplyContent(content),
|
||||||
onClearReplyError: () => this.store.clearReplyError(),
|
onClearReplyError: () => this.store.clearReplyError(),
|
||||||
replyPlaceholder: this.config.commentPlaceholder,
|
replyPlaceholder: this.config.commentPlaceholder,
|
||||||
onPrevPage: () => this.store.goToPage(state.pagination.page - 1),
|
onPrevPage: () => {
|
||||||
onNextPage: () => this.store.goToPage(state.pagination.page + 1),
|
const currentState = this.store.store.getState();
|
||||||
|
this.store.goToPage(currentState.pagination.page - 1);
|
||||||
|
},
|
||||||
|
onNextPage: () => {
|
||||||
|
const currentState = this.store.store.getState();
|
||||||
|
this.store.goToPage(currentState.pagination.page + 1);
|
||||||
|
},
|
||||||
onGoToPage: (page) => this.store.goToPage(page),
|
onGoToPage: (page) => this.store.goToPage(page),
|
||||||
onLikeComment: (commentId, isLike) => {
|
onLikeComment: (commentId, isLike) => {
|
||||||
if (this.store && typeof this.store.likeComment === 'function') {
|
if (this.store && typeof this.store.likeComment === 'function') {
|
||||||
|
|||||||
@@ -660,6 +660,10 @@
|
|||||||
color: var(--cwd-primary, #0969da);
|
color: var(--cwd-primary, #0969da);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cwd-comment-like-button-liked {
|
||||||
|
color: var(--cwd-primary, #0969da);
|
||||||
|
}
|
||||||
|
|
||||||
.cwd-comment-like-icon-wrapper {
|
.cwd-comment-like-icon-wrapper {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
import * as emoji from 'node-emoji';
|
||||||
|
|
||||||
// 配置 marked
|
// 配置 marked
|
||||||
try {
|
try {
|
||||||
@@ -11,6 +12,30 @@ try {
|
|||||||
console.error('Failed to configure marked:', e);
|
console.error('Failed to configure marked:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换文本中的 emoji 代码为实际 emoji
|
||||||
|
* @param {string} text 包含 emoji 代码的文本
|
||||||
|
* @returns {string} 替换后的文本
|
||||||
|
*/
|
||||||
|
function replaceEmoji(text) {
|
||||||
|
if (!text) return text;
|
||||||
|
try {
|
||||||
|
return emoji.emojify(text);
|
||||||
|
} catch (e) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换 HTML 中的 emoji 代码为实际 emoji
|
||||||
|
* @param {string} html 包含 emoji 代码的 HTML
|
||||||
|
* @returns {string} 替换后的 HTML
|
||||||
|
*/
|
||||||
|
export function replaceEmojiInHtml(html) {
|
||||||
|
if (!html) return html;
|
||||||
|
return replaceEmoji(html);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染 Markdown 为 HTML,并进行净化
|
* 渲染 Markdown 为 HTML,并进行净化
|
||||||
* @param {string} content Markdown 内容
|
* @param {string} content Markdown 内容
|
||||||
@@ -19,7 +44,8 @@ try {
|
|||||||
export function renderMarkdown(content) {
|
export function renderMarkdown(content) {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
try {
|
try {
|
||||||
const html = marked.parse(content);
|
const contentWithEmoji = replaceEmoji(content);
|
||||||
|
const html = marked.parse(contentWithEmoji);
|
||||||
// marked.parse can return a Promise if async is enabled, but we are using sync mode
|
// marked.parse can return a Promise if async is enabled, but we are using sync mode
|
||||||
// Just in case, handle potential Promise (though unlikely with current config)
|
// Just in case, handle potential Promise (though unlikely with current config)
|
||||||
if (html instanceof Promise) {
|
if (html instanceof Promise) {
|
||||||
|
|||||||
Reference in New Issue
Block a user