feat: 走心评论(精选评论)+ 移动端评论框底栏布局修复

- 底栏:≤480px 压缩按钮内边距,SE 等小屏单行放下;换行时按钮组右对齐
- Worker:comments 加 is_featured;PUT /comments/:id 支持切换;新增公开接口
  GET /api/v2/featured/comments(排除悄悄话/待审/已删,服务端渲染 markdown)
- 前端:评论区注入「精选」按钮(回复右侧,仅管理员可见)
- 关于页:走心评论板块(标题寄语通栏 + 卡片瀑布流),排在热力图上方
This commit is contained in:
zqlit committed 2026-10-05 12:08:33 +08:00
1 parent 3d7d7bd396
commit 1b71fba413
10 files changed
+409 -2

No files matched your search

+1
View File
@@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS comments (
is_pinned INTEGER NOT NULL DEFAULT 0,
is_verified INTEGER NOT NULL DEFAULT 0,
is_anonymous INTEGER NOT NULL DEFAULT 0, -- 匿名评论:对外显示「匿名」,真实身份只留后台
is_featured INTEGER NOT NULL DEFAULT 0, -- 走心评论:管理员精选,关于页展示(2026-10-05 追加,存量库需 ALTER)
vote_up INTEGER NOT NULL DEFAULT 0,
vote_down INTEGER NOT NULL DEFAULT 0,
ua TEXT NOT NULL DEFAULT '',
+1
View File
@@ -56,6 +56,7 @@ router.post('/comments', C.createComment);
router.get('/comments/:id', C.getComment);
router.put('/comments/:id', C.updateComment);
router.del('/comments/:id', C.deleteComment);
router.get('/featured/comments', C.listFeaturedComments);
// ---------------------------------------------------------------- 投票 / 浏览量
// ★ 投票:Artalk 客户端把「方向」编进 target_name(comment_up / comment_down /
+1
View File
@@ -245,6 +245,7 @@ export async function cookComments(
is_allow_reply: !c.is_collapsed && !c.is_pending,
is_verified: user?.is_admin ? true : !!c.is_verified,
is_anonymous: isAnon,
is_featured: !!c.is_featured,
rid: c.rid,
badge_name: user?.badge_name ?? '',
badge_color: user?.badge_color ?? '',
+67
View File
@@ -6,6 +6,7 @@ import {
findOrCreatePage,
findOrCreateSite,
findOrCreateUser,
findPagesByKeys,
findUserById,
hasCaptchaPassed,
isCaptchaEnabled,
@@ -18,6 +19,7 @@ import {
import { cookComments, cookPage, pageAccessibleUrl } from '../lib/cook';
import { mailEnabled, notifyByEmail } from '../lib/mail';
import { isAdminByNameEmail, isAdminRequest } from '../lib/session';
import { renderMarkdown } from '../lib/md';
import { md5Lower } from '../lib/md5';
import {
fail,
@@ -535,6 +537,70 @@ export async function createComment(ctx: Ctx): Promise<Response> {
return ok(cooked);
}
// ============================================================ GET /featured/comments
/**
* 走心评论公开列表:管理员精选的评论,关于页展示用。
*
* 只给「精选且未删未待审」的;悄悄话评论(is_anonymous)永远不进这里 ——
* 它的内容本来就只对博主可见,精选展示等于泄露。
* content_html 是服务端渲染好的 markdown(与后台 content_marked 同一渲染器),
* 前端直接 innerHTML,不用再带一份 marked。
*/
export async function listFeaturedComments(ctx: Ctx): Promise<Response> {
const { env, url } = ctx;
const limit = Math.min(Math.max(qInt(url, 'limit', 8), 1), 20);
const rows = (
await env.DB.prepare(
`SELECT c.* FROM comments c
WHERE c.is_featured = 1 AND c.deleted_at = 0 AND c.is_pending = 0 AND c.is_anonymous = 0
ORDER BY c.created_at DESC LIMIT ?`,
)
.bind(limit)
.all<CommentRow>()
).results ?? [];
if (!rows.length) return ok({ comments: [] });
const cooked = await cookComments(env, rows, { ipRegion: false, includeMarked: false });
// 页面标题:按 (site_name, page_key) 去重后一次捞齐,避免 N+1
const titleMap = new Map<string, string>();
const bySite = new Map<string, string[]>();
for (const r of rows) {
const arr = bySite.get(r.site_name) ?? [];
arr.push(r.page_key);
bySite.set(r.site_name, arr);
}
for (const [siteName, keys] of bySite) {
for (const [k, v] of await findPagesByKeys(env, keys, siteName)) {
titleMap.set(`${siteName}\u0000${k}`, v.title || '');
}
}
const out = cooked.map((c, i) => ({
id: c.id,
nick: c.nick,
link: c.link,
email_encrypted: c.email_encrypted, // 前端拼 weavatar 头像(与评论区同一套)
badge_name: c.badge_name,
badge_color: c.badge_color,
title_name: c.title_name,
rank_name: c.rank_name,
rank_color: c.rank_color,
rank_bg: c.rank_bg,
date: c.date,
content_html: renderMarkdown(rows[i].content),
page_key: c.page_key,
page_url: c.page_url,
page_title: titleMap.get(`${c.site_name}\u0000${c.page_key}`) || '',
}));
return ok({ comments: out });
}
// ============================================================ 单条操作
export async function getComment(ctx: Ctx): Promise<Response> {
@@ -585,6 +651,7 @@ export async function updateComment(ctx: Ctx): Promise<Response> {
['is_pending', 'is_pending'],
['is_pinned', 'is_pinned'],
['is_verified', 'is_verified'],
['is_featured', 'is_featured'],
] as const) {
if (body[field] !== undefined) {
sets.push(`${col} = ?`);
+4
View File
@@ -113,6 +113,8 @@ export interface CommentRow {
is_pinned: number;
is_verified: number;
is_anonymous: number;
/** 走心评论:管理员手动精选,在关于页展示 */
is_featured: number;
vote_up: number;
vote_down: number;
ua: string;
@@ -142,6 +144,8 @@ export interface CookedComment {
is_verified: boolean;
/** 该评论是否匿名发表(管理员请求时字段原样保留,内容是否脱敏看服务端) */
is_anonymous: boolean;
/** 走心评论(管理员精选),前台据此渲染「精选/取消精选」按钮 */
is_featured: boolean;
rid: number;
badge_name: string;
badge_color: string;