feat(评论): 添加评论置顶功能

- 在评论表中添加 priority 字段用于控制置顶权重
- 修改所有评论查询接口按 priority 和 created 排序
- 在管理后台添加置顶权重编辑功能
- 在评论列表显示置顶标识
This commit is contained in:
anghunk
2026-01-21 21:25:08 +08:00
parent 47c2eed6b5
commit 3871620fed
7 changed files with 51 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ export type CommentItem = {
contentText: string; contentText: string;
contentHtml: string; contentHtml: string;
status: string; status: string;
priority?: number;
ua?: string | null; ua?: string | null;
}; };
@@ -118,6 +119,7 @@ export function updateComment(data: {
postSlug?: string; postSlug?: string;
contentText: string; contentText: string;
status?: string; status?: string;
priority?: number;
}): Promise<{ message: string }> { }): Promise<{ message: string }> {
return put<{ message: string }>('/admin/comments/update', { return put<{ message: string }>('/admin/comments/update', {
id: data.id, id: data.id,
@@ -126,7 +128,8 @@ export function updateComment(data: {
url: data.url ?? null, url: data.url ?? null,
postSlug: data.postSlug, postSlug: data.postSlug,
content: data.contentText, content: data.contentText,
status: data.status status: data.status,
priority: data.priority
}); });
} }

View File

@@ -76,9 +76,14 @@
> >
</div> </div>
<div class="table-cell table-cell-status"> <div class="table-cell table-cell-status">
<span class="cell-status" :class="`cell-status-${item.status}`"> <div class="cell-status-wrapper">
{{ formatStatus(item.status) }} <span class="cell-status" :class="`cell-status-${item.status}`">
</span> {{ formatStatus(item.status) }}
</span>
<span v-if="item.priority && item.priority > 1" class="cell-pin-flag">
置顶 {{ item.priority }}
</span>
</div>
</div> </div>
<div class="table-cell table-cell-actions"> <div class="table-cell table-cell-actions">
<div class="table-actions"> <div class="table-actions">
@@ -176,7 +181,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="editVisible" class="modal-overlay" @click.self="closeEdit"> <div v-if="editVisible" class="modal-overlay">
<div class="modal"> <div class="modal">
<h3 class="modal-title">编辑评论</h3> <h3 class="modal-title">编辑评论</h3>
<div v-if="editForm" class="modal-body"> <div v-if="editForm" class="modal-body">
@@ -212,6 +217,10 @@
<option value="rejected">已拒绝</option> <option value="rejected">已拒绝</option>
</select> </select>
</div> </div>
<div class="form-item">
<label class="form-label">置顶权重1 为不置顶,数值越大越靠前)</label>
<input v-model.number="editForm.priority" class="form-input" type="number" min="1" />
</div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="modal-btn secondary" type="button" @click="closeEdit"> <button class="modal-btn secondary" type="button" @click="closeEdit">
@@ -267,6 +276,7 @@ const editForm = ref<{
postSlug: string; postSlug: string;
contentText: string; contentText: string;
status: string; status: string;
priority: number;
} | null>(null); } | null>(null);
const domainOptions = ref<string[]>([]); const domainOptions = ref<string[]>([]);
@@ -443,6 +453,7 @@ function openEdit(item: CommentItem) {
postSlug: item.postSlug || "", postSlug: item.postSlug || "",
contentText: item.contentText, contentText: item.contentText,
status: item.status, status: item.status,
priority: typeof item.priority === "number" && Number.isFinite(item.priority) ? item.priority : 1,
}; };
editVisible.value = true; editVisible.value = true;
} }
@@ -465,6 +476,7 @@ async function submitEdit() {
const trimmedContent = current.contentText.trim(); const trimmedContent = current.contentText.trim();
const trimmedUrl = current.url.trim(); const trimmedUrl = current.url.trim();
const trimmedPostSlug = current.postSlug.trim(); const trimmedPostSlug = current.postSlug.trim();
const priorityValue = typeof current.priority === "number" && Number.isFinite(current.priority) ? current.priority : 1;
const commentIndex = comments.value.findIndex((c) => c.id === current.id); const commentIndex = comments.value.findIndex((c) => c.id === current.id);
const existingComment = commentIndex !== -1 ? comments.value[commentIndex] : null; const existingComment = commentIndex !== -1 ? comments.value[commentIndex] : null;
const newPostSlug = trimmedPostSlug || existingComment?.postSlug || ""; const newPostSlug = trimmedPostSlug || existingComment?.postSlug || "";
@@ -483,6 +495,7 @@ async function submitEdit() {
postSlug: newPostSlug, postSlug: newPostSlug,
contentText: trimmedContent, contentText: trimmedContent,
status: current.status, status: current.status,
priority: priorityValue,
}); });
if (commentIndex !== -1) { if (commentIndex !== -1) {
comments.value[commentIndex] = { comments.value[commentIndex] = {
@@ -493,6 +506,7 @@ async function submitEdit() {
postSlug: newPostSlug, postSlug: newPostSlug,
contentText: trimmedContent, contentText: trimmedContent,
status: current.status, status: current.status,
priority: priorityValue,
}; };
} }
closeEdit(); closeEdit();

View File

@@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS Comment (
content_text TEXT NOT NULL, content_text TEXT NOT NULL,
content_html TEXT NOT NULL, content_html TEXT NOT NULL,
parent_id INTEGER, parent_id INTEGER,
priority INTEGER NOT NULL DEFAULT 1,
status TEXT DEFAULT 'approved', status TEXT DEFAULT 'approved',
-- 建立自引用外键约束(父子评论关系) -- 建立自引用外键约束(父子评论关系)
FOREIGN KEY (parent_id) REFERENCES Comment (id) ON DELETE SET NULL FOREIGN KEY (parent_id) REFERENCES Comment (id) ON DELETE SET NULL

View File

@@ -4,7 +4,7 @@ import { Bindings } from '../../bindings';
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => { export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
try { try {
const { results } = await c.env.CWD_DB.prepare( const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY created DESC' 'SELECT * FROM Comment ORDER BY priority DESC, created DESC'
).all(); ).all();
return c.json(results); return c.json(results);

View File

@@ -27,7 +27,7 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
.first<{ count: number }>(); .first<{ count: number }>();
const { results } = await c.env.CWD_DB.prepare( const { results } = await c.env.CWD_DB.prepare(
`SELECT * FROM Comment ${whereSql} ORDER BY created DESC LIMIT ? OFFSET ?` `SELECT * FROM Comment ${whereSql} ORDER BY priority DESC, created DESC LIMIT ? OFFSET ?`
) )
.bind(...params, limit, offset) .bind(...params, limit, offset)
.all(); .all();
@@ -52,6 +52,7 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
contentText: row.content_text, contentText: row.content_text,
contentHtml: row.content_html, contentHtml: row.content_html,
status: row.status, status: row.status,
priority: row.priority,
ua: row.ua, ua: row.ua,
avatar: await getCravatar(row.email, avatarPrefix || undefined) avatar: await getCravatar(row.email, avatarPrefix || undefined)
})) }))

View File

@@ -25,10 +25,10 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
} }
const existing = await c.env.CWD_DB.prepare( const existing = await c.env.CWD_DB.prepare(
'SELECT id, status, post_slug FROM Comment WHERE id = ?' 'SELECT id, status, post_slug, priority FROM Comment WHERE id = ?'
) )
.bind(id) .bind(id)
.first<{ id: number; status: string; post_slug: string }>(); .first<{ id: number; status: string; post_slug: string; priority: number | null }>();
if (!existing) { if (!existing) {
return c.json({ message: 'Comment not found' }, 404); return c.json({ message: 'Comment not found' }, 404);
@@ -38,6 +38,7 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
const rawEmail = typeof body.email === 'string' ? body.email : ''; const rawEmail = typeof body.email === 'string' ? body.email : '';
const rawUrl = typeof body.url === 'string' ? body.url : ''; const rawUrl = typeof body.url === 'string' ? body.url : '';
const rawStatus = typeof body.status === 'string' ? body.status : existing.status; const rawStatus = typeof body.status === 'string' ? body.status : existing.status;
const rawPriority = body.priority;
const hasPostSlugField = const hasPostSlugField =
Object.prototype.hasOwnProperty.call(body, 'postSlug') || Object.prototype.hasOwnProperty.call(body, 'postSlug') ||
Object.prototype.hasOwnProperty.call(body, 'post_slug'); Object.prototype.hasOwnProperty.call(body, 'post_slug');
@@ -63,6 +64,22 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
? (rawPostSlug.trim() || existing.post_slug) ? (rawPostSlug.trim() || existing.post_slug)
: existing.post_slug; : existing.post_slug;
let priority: number = typeof existing.priority === 'number' && Number.isFinite(existing.priority)
? existing.priority
: 1;
if (rawPriority !== undefined && rawPriority !== null) {
const parsed =
typeof rawPriority === 'number'
? rawPriority
: typeof rawPriority === 'string' && rawPriority.trim()
? Number.parseInt(rawPriority.trim(), 10)
: NaN;
if (Number.isFinite(parsed) && parsed >= 1) {
priority = parsed;
}
}
if (!name) { if (!name) {
return c.json({ message: '昵称不能为空' }, 400); return c.json({ message: '昵称不能为空' }, 400);
} }
@@ -90,9 +107,9 @@ export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
}); });
const { success } = await c.env.CWD_DB.prepare( const { success } = await c.env.CWD_DB.prepare(
'UPDATE Comment SET name = ?, email = ?, url = ?, content_text = ?, content_html = ?, status = ?, post_slug = ? WHERE id = ?' 'UPDATE Comment SET name = ?, email = ?, url = ?, content_text = ?, content_html = ?, status = ?, post_slug = ?, priority = ? WHERE id = ?'
) )
.bind(name, email, url, contentText, contentHtml, status, postSlug, id) .bind(name, email, url, contentText, contentHtml, status, postSlug, priority, id)
.run(); .run();
if (!success) { if (!success) {

View File

@@ -13,14 +13,13 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
if (!post_slug) return c.json({ message: "post_slug is required" }, 400) if (!post_slug) return c.json({ message: "post_slug is required" }, 400)
try { try {
// 1. 查询审核通过的评论
const query = ` const query = `
SELECT id, name, email, url, content_text as contentText, SELECT id, name, email, url, content_text as contentText,
content_html as contentHtml, created, parent_id as parentId, content_html as contentHtml, created, parent_id as parentId,
post_slug as postSlug post_slug as postSlug, priority
FROM Comment FROM Comment
WHERE post_slug = ? AND status = "approved" WHERE post_slug = ? AND status = "approved"
ORDER BY created DESC ORDER BY priority DESC, created DESC
` `
const { results } = await c.env.CWD_DB.prepare(query).bind(post_slug).all() const { results } = await c.env.CWD_DB.prepare(query).bind(post_slug).all()