feat(评论系统): 添加评论配置和头像显示功能

- 新增评论配置管理功能,包括博主邮箱、标签和头像前缀设置
- 在评论列表显示用户头像
- 实现服务端配置存储和获取逻辑
- 优化前端设置页面,拆分不同配置项
- 修改开发预览页面配置保存逻辑
This commit is contained in:
anghunk
2026-01-20 09:23:46 +08:00
parent fe463125ea
commit 456771bae3
8 changed files with 304 additions and 66 deletions

View File

@@ -1,41 +1,54 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { getCravatar } from '../../utils/getAvatar';
const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
const page = parseInt(c.req.query('page') || '1');
const limit = 10;
const offset = (page - 1) * limit;
const page = parseInt(c.req.query('page') || '1');
const limit = 10;
const offset = (page - 1) * limit;
// 1. 获取总数
const totalCount = await c.env.CWD_DB.prepare(
"SELECT COUNT(*) as count FROM Comment"
).first<{ count: number }>();
const totalCount = await c.env.CWD_DB.prepare(
'SELECT COUNT(*) as count FROM Comment'
).first<{ count: number }>();
// 2. 分页查询数据
const { results } = await c.env.CWD_DB.prepare(
`SELECT * FROM Comment ORDER BY pub_date DESC LIMIT ? OFFSET ?`
).bind(limit, offset).all();
const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY pub_date DESC LIMIT ? OFFSET ?'
)
.bind(limit, offset)
.all();
// 3. 映射字段名以符合你的 API 规范
const data = results.map((row: any) => ({
id: row.id,
pubDate: row.pub_date,
author: row.author,
email: row.email,
postSlug: row.post_slug,
url: row.url,
ipAddress: row.ip_address,
contentText: row.content_text,
contentHtml: row.content_html,
status: row.status
}));
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const avatarRow = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind(COMMENT_AVATAR_PREFIX_KEY)
.first<{ value: string }>();
const avatarPrefix = avatarRow?.value || null;
return c.json({
data,
pagination: {
page,
limit,
total: Math.ceil((totalCount?. count || 0) / limit)
}
});
};
const data = await Promise.all(
results.map(async (row: any) => ({
id: row.id,
pubDate: row.pub_date,
author: row.author,
email: row.email,
postSlug: row.post_slug,
url: row.url,
ipAddress: row.ip_address,
contentText: row.content_text,
contentHtml: row.content_html,
status: row.status,
avatar: await getCravatar(row.email, avatarPrefix || undefined)
}))
);
return c.json({
data,
pagination: {
page,
limit,
total: Math.ceil(((totalCount?.count as number) || 0) / limit)
}
});
};

View File

@@ -1,8 +1,8 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Bindings } from './bindings';
import { customCors } from './utils/cors';
import { adminAuth } from './utils/auth';
import { isValidEmail } from './utils/email';
import { getComments } from './api/public/getComments';
import { postComment } from './api/public/postComment';
@@ -16,6 +16,64 @@ import { setAdminEmail } from './api/admin/setAdminEmail';
const app = new Hono<{ Bindings: Bindings }>();
const VERSION = 'v0.0.1';
const COMMENT_ADMIN_EMAIL_KEY = 'comment_admin_email';
const COMMENT_ADMIN_BADGE_KEY = 'comment_admin_badge';
const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
async function loadCommentSettings(env: Bindings) {
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const keys = [COMMENT_ADMIN_EMAIL_KEY, COMMENT_ADMIN_BADGE_KEY, COMMENT_AVATAR_PREFIX_KEY];
const { results } = await env.CWD_DB.prepare(
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?)'
)
.bind(...keys)
.all<{ key: string; value: string }>();
const map = new Map<string, string>();
for (const row of results) {
if (row && row.key) {
map.set(row.key, row.value);
}
}
return {
adminEmail: map.get(COMMENT_ADMIN_EMAIL_KEY) ?? null,
adminBadge: map.get(COMMENT_ADMIN_BADGE_KEY) ?? null,
avatarPrefix: map.get(COMMENT_AVATAR_PREFIX_KEY) ?? null
};
}
async function saveCommentSettings(
env: Bindings,
settings: { adminEmail?: string; adminBadge?: string; avatarPrefix?: string }
) {
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const entries: { key: string; value: string | null | undefined }[] = [
{ key: COMMENT_ADMIN_EMAIL_KEY, value: settings.adminEmail },
{ key: COMMENT_ADMIN_BADGE_KEY, value: settings.adminBadge },
{ key: COMMENT_AVATAR_PREFIX_KEY, value: settings.avatarPrefix }
];
for (const entry of entries) {
if (entry.value !== undefined) {
const value = entry.value === null ? '' : entry.value;
const trimmed = typeof value === 'string' ? value.trim() : value;
if (trimmed) {
await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
.bind(entry.key, trimmed)
.run();
} else {
await env.CWD_DB.prepare('DELETE FROM Settings WHERE key = ?').bind(entry.key).run();
}
}
}
}
app.use('*', async (c, next) => {
console.log('Request:start', {
method: c.req.method,
@@ -49,6 +107,14 @@ app.get('/', (c) => {
app.get('/api/comments', getComments);
app.post('/api/comments', postComment);
app.get('/api/config/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载评论配置失败' }, 500);
}
});
app.post('/admin/login', adminLogin);
app.use('/admin/*', adminAuth);
@@ -58,5 +124,39 @@ app.put('/admin/comments/status', updateStatus);
// 设置接口
app.get('/admin/settings/email', getAdminEmail);
app.put('/admin/settings/email', setAdminEmail);
app.get('/admin/settings/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载评论配置失败' }, 500);
}
});
app.put('/admin/settings/comments', async (c) => {
try {
const body = await c.req.json();
const rawAdminEmail = typeof body.adminEmail === 'string' ? body.adminEmail : '';
const rawAdminBadge = typeof body.adminBadge === 'string' ? body.adminBadge : '';
const rawAvatarPrefix = typeof body.avatarPrefix === 'string' ? body.avatarPrefix : '';
const adminEmail = rawAdminEmail.trim();
const adminBadge = rawAdminBadge.trim();
const avatarPrefix = rawAvatarPrefix.trim();
if (adminEmail && !isValidEmail(adminEmail)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
await saveCommentSettings(c.env, {
adminEmail,
adminBadge,
avatarPrefix
});
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message || '保存失败' }, 500);
}
});
export default app;