feat: 走心评论(精选评论)+ 移动端评论框底栏布局修复
- 底栏:≤480px 压缩按钮内边距,SE 等小屏单行放下;换行时按钮组右对齐 - Worker:comments 加 is_featured;PUT /comments/:id 支持切换;新增公开接口 GET /api/v2/featured/comments(排除悄悄话/待审/已删,服务端渲染 markdown) - 前端:评论区注入「精选」按钮(回复右侧,仅管理员可见) - 关于页:走心评论板块(标题寄语通栏 + 卡片瀑布流),排在热力图上方
This commit is contained in:
1 parent
3d7d7bd396
commit
1b71fba413
10 files changed
+409
-2
No files matched your search
@@ -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 '',
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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} = ?`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user