feat(admin): 实现评论数据导入功能并优化界面

- 添加导入评论的API接口和前端实现
- 在数据管理页面增加导入功能,支持选择来源和上传JSON文件
- 优化导航菜单和页面标题的命名
- 移除调试用的console.log语句
- 更新相关API文档
This commit is contained in:
anghunk
2026-01-20 13:27:50 +08:00
parent e826b7cbef
commit 520cfcc447
9 changed files with 341 additions and 21 deletions

View File

@@ -0,0 +1,87 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
try {
const body = await c.req.json();
const comments = Array.isArray(body) ? body : [body];
if (comments.length === 0) {
return c.json({ message: '导入数据为空' }, 400);
}
// 按 ID 升序排序,防止因外键约束导致插入失败(子评论先于父评论插入)
comments.sort((a: any, b: any) => {
const idA = a.id || 0;
const idB = b.id || 0;
return idA - idB;
});
// 简单的验证,确保至少包含必要字段
// 实际上 Twikoo 的导出格式可能不同,但用户提供的 JSON 结构与 cwd-comments 几乎一致
// 我们假设用户已经转换或者这就是他们想要的格式
// 如果字段名不匹配,可能需要做映射。既然用户给出了明确的 JSON 结构,我们直接按照这个结构处理。
const stmts = comments.map((comment: any) => {
const {
id,
pub_date,
post_slug,
author,
email,
url,
ip_address,
device,
os,
browser,
user_agent,
content_text,
content_html,
parent_id,
status
} = comment;
// 如果 id 存在,我们尝试保留它。如果冲突,可能需要处理(这里暂且假设是空库导入或者不冲突)
// 如果是导入 Twikoo通常 Twikoo 没有 id (是 _id ObjectId),或者是其他格式。
// 但按照用户给的 JSON是有 id 的。
// 构建 SQL。
// 能够处理 id 存在或不存在的情况
const fields = [
'pub_date', 'post_slug', 'author', 'email', 'url',
'ip_address', 'device', 'os', 'browser', 'user_agent',
'content_text', 'content_html', 'parent_id', 'status'
];
const values = [
pub_date, post_slug, author, email, url,
ip_address, device, os, browser, user_agent,
content_text, content_html, parent_id, status
].map(v => v === undefined ? null : v);
if (id) {
fields.unshift('id');
values.unshift(id);
}
const placeholders = fields.map(() => '?').join(', ');
const sql = `INSERT OR REPLACE INTO Comment (${fields.join(', ')}) VALUES (${placeholders})`;
return c.env.CWD_DB.prepare(sql).bind(...values);
});
// D1 的 batch 限制一次最多 100 条? 或者是 body size 限制。
// 如果数量很大,需要分批。这里先假设数量不多,直接 batch。
// 为了安全起见,可以分批处理,比如每 50 条。
const BATCH_SIZE = 50;
for (let i = 0; i < stmts.length; i += BATCH_SIZE) {
const batch = stmts.slice(i, i + BATCH_SIZE);
await c.env.CWD_DB.batch(batch);
}
return c.json({ message: `成功导入 ${comments.length} 条评论` });
} catch (e: any) {
console.error(e);
return c.json({ message: e.message || '导入失败' }, 500);
}
};

View File

@@ -14,6 +14,7 @@ import { adminLogin } from './api/admin/login';
import { deleteComment } from './api/admin/deleteComment';
import { listComments } from './api/admin/listComments';
import { exportComments } from './api/admin/exportComments';
import { importComments } from './api/admin/importComments';
import { updateStatus } from './api/admin/updateStatus';
import { getAdminEmail } from './api/admin/getAdminEmail';
import { setAdminEmail } from './api/admin/setAdminEmail';
@@ -151,6 +152,7 @@ app.use('/admin/*', adminAuth);
app.delete('/admin/comments/delete', deleteComment);
app.get('/admin/comments/list', listComments);
app.get('/admin/comments/export', exportComments);
app.post('/admin/comments/import', importComments);
app.put('/admin/comments/status', updateStatus);
app.get('/admin/settings/email', getAdminEmail);
app.put('/admin/settings/email', setAdminEmail);