完善论坛配置与发帖体验:TipTap 富文本、图片上传、修订历史、Feed 排序与后台参数管理。

同步更新 README 与 ROADMAP,反映最新功能与开发状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-16 04:11:38 +08:00
parent 1d273066b0
commit b451703642
73 changed files with 1943 additions and 752 deletions

View File

@@ -0,0 +1,104 @@
/**
* 补全评论、点赞、置顶/锁定(帖子已存在时使用)
* 用法node scripts/seed-interactions.mjs
*/
const BASE = process.env.SEED_BASE || 'http://localhost:3000';
let cookie = '';
function captureCookie(res) {
const raw = res.headers.getSetCookie?.() ?? [];
for (const line of raw) {
const part = line.split(';')[0].trim();
if (part.startsWith('jiang13_token=')) cookie = part;
}
}
async function request(path, opts = {}) {
const headers = { ...(opts.headers || {}) };
if (cookie) headers.Cookie = cookie;
if (opts.body instanceof FormData) delete headers['Content-Type'];
const res = await fetch(`${BASE}${path}`, { ...opts, headers });
captureCookie(res);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `${res.status} ${path}`);
return data;
}
async function login(username, password) {
const fd = new FormData();
fd.append('username', username);
fd.append('password', password);
await request('/api/login', { method: 'POST', body: fd });
}
async function addComment(postId, content, replyTo) {
const fd = new FormData();
fd.append('content', content);
if (replyTo) fd.append('reply_to', String(replyTo));
const data = await request(`/api/posts/${postId}/comments`, { method: 'POST', body: fd });
return data.id;
}
async function likePost(postId) {
await request(`/api/posts/${postId}/like`, { method: 'POST' });
}
async function pinPost(postId) {
await request(`/api/admin/posts/${postId}/pin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pinned: true }),
});
}
async function lockPost(postId) {
await request(`/api/admin/posts/${postId}/lock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ locked: true }),
});
}
async function main() {
const { posts } = await request('/api/posts?page=1&size=50');
const list = posts ?? [];
if (!list.length) throw new Error('没有帖子,请先运行 node scripts/seed.mjs');
const byTitle = (kw) => list.find((p) => p.title.includes(kw));
const first = list[0];
const qa = byTitle('embed') ?? first;
const hot = list.filter((p) => p.title.includes('热门候选'));
const lockTarget = byTitle('夜班') ?? list.at(-1);
console.log('补全评论与互动…');
await login('alice', 'alice123');
await addComment(first.id, '第一条评论,测试楼层显示。');
const aliceC = await addComment(first.id, '同意,排版帖很有参考价值。');
await addComment(qa.id, '同问embed 路径我也踩过坑。');
await login('bob', 'bob123');
await addComment(first.id, '引用回复测试', aliceC);
for (const p of hot.slice(0, 2)) await addComment(p.id, '热门帖评论一条。');
await login('admin', 'admin123');
await addComment(first.id, '管理员也来回复一楼。');
for (const p of hot) await likePost(p.id);
await login('alice', 'alice123');
for (const p of hot) await likePost(p.id);
await login('bob', 'bob123');
for (const p of hot) await likePost(p.id);
await login('admin', 'admin123');
await pinPost(first.id);
if (list[1]) await pinPost(list[1].id);
if (lockTarget) await lockPost(lockTarget.id);
const stats = await request('/api/stats');
console.log(`完成。帖子 ${stats.posts} · 会员 ${stats.users}`);
}
main().catch((e) => {
console.error('失败:', e.message);
process.exit(1);
});