feat(评论): 添加评论置顶功能
- 在评论表中添加 priority 字段用于控制置顶权重 - 修改所有评论查询接口按 priority 和 created 排序 - 在管理后台添加置顶权重编辑功能 - 在评论列表显示置顶标识
This commit is contained in:
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user