feat(评论管理): 添加编辑评论功能
在评论管理页面添加编辑功能,允许管理员修改评论的昵称、邮箱、网址、内容和状态
This commit is contained in:
@@ -110,6 +110,24 @@ export function updateCommentStatus(id: number, status: string): Promise<{ messa
|
||||
return put<{ message: string }>(`/admin/comments/status?id=${id}&status=${encodeURIComponent(status)}`);
|
||||
}
|
||||
|
||||
export function updateComment(data: {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
url?: string | null;
|
||||
contentText: string;
|
||||
status?: string;
|
||||
}): Promise<{ message: string }> {
|
||||
return put<{ message: string }>('/admin/comments/update', {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
url: data.url ?? null,
|
||||
content: data.contentText,
|
||||
status: data.status
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchAdminEmail(): Promise<AdminEmailResponse> {
|
||||
return get<AdminEmailResponse>('/admin/settings/email');
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
<button class="table-action" @click="openEdit(item)">编辑</button>
|
||||
<button
|
||||
class="table-action table-action-danger"
|
||||
@click="removeComment(item)"
|
||||
@@ -187,6 +188,55 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="editVisible" class="modal-overlay" @click.self="closeEdit">
|
||||
<div class="modal">
|
||||
<h3 class="modal-title">编辑评论</h3>
|
||||
<div v-if="editForm" class="modal-body">
|
||||
<div class="form-item">
|
||||
<label class="form-label">访客昵称</label>
|
||||
<input v-model="editForm.name" class="form-input" type="text" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">访客邮箱</label>
|
||||
<input v-model="editForm.email" class="form-input" type="email" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">访客网址</label>
|
||||
<input v-model="editForm.url" class="form-input" type="text" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">评论内容</label>
|
||||
<textarea
|
||||
v-model="editForm.contentText"
|
||||
class="form-input"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">评论状态</label>
|
||||
<select v-model="editForm.status" class="form-input">
|
||||
<option value="approved">已通过</option>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn secondary" type="button" @click="closeEdit">
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn primary"
|
||||
type="button"
|
||||
:disabled="editSaving"
|
||||
@click="submitEdit"
|
||||
>
|
||||
<span v-if="editSaving">保存中...</span>
|
||||
<span v-else>保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -200,6 +250,7 @@ import {
|
||||
fetchCommentStats,
|
||||
deleteComment,
|
||||
updateCommentStatus,
|
||||
updateComment,
|
||||
blockIp,
|
||||
blockEmail,
|
||||
} from "../api/admin";
|
||||
@@ -214,6 +265,16 @@ const error = ref("");
|
||||
const statusFilter = ref("");
|
||||
const domainFilter = ref("");
|
||||
const jumpPageInput = ref("");
|
||||
const editVisible = ref(false);
|
||||
const editSaving = ref(false);
|
||||
const editForm = ref<{
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
url: string;
|
||||
contentText: string;
|
||||
status: string;
|
||||
} | null>(null);
|
||||
|
||||
const domainOptions = ref<string[]>([]);
|
||||
|
||||
@@ -371,6 +432,65 @@ async function handleBlockEmail(item: CommentItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(item: CommentItem) {
|
||||
editForm.value = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
email: item.email,
|
||||
url: item.url || "",
|
||||
contentText: item.contentText,
|
||||
status: item.status,
|
||||
};
|
||||
editVisible.value = true;
|
||||
}
|
||||
|
||||
function closeEdit() {
|
||||
if (editSaving.value) {
|
||||
return;
|
||||
}
|
||||
editVisible.value = false;
|
||||
editForm.value = null;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editForm.value || editSaving.value) {
|
||||
return;
|
||||
}
|
||||
const current = editForm.value;
|
||||
if (!current.name.trim() || !current.email.trim() || !current.contentText.trim()) {
|
||||
error.value = "昵称、邮箱和内容不能为空";
|
||||
return;
|
||||
}
|
||||
editSaving.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
await updateComment({
|
||||
id: current.id,
|
||||
name: current.name.trim(),
|
||||
email: current.email.trim(),
|
||||
url: current.url.trim() || null,
|
||||
contentText: current.contentText,
|
||||
status: current.status,
|
||||
});
|
||||
const index = comments.value.findIndex((c) => c.id === current.id);
|
||||
if (index !== -1) {
|
||||
comments.value[index] = {
|
||||
...comments.value[index],
|
||||
name: current.name.trim(),
|
||||
email: current.email.trim(),
|
||||
url: current.url.trim() || null,
|
||||
contentText: current.contentText,
|
||||
status: current.status,
|
||||
};
|
||||
}
|
||||
closeEdit();
|
||||
} catch (e: any) {
|
||||
error.value = e.message || "更新评论失败";
|
||||
} finally {
|
||||
editSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const p = route.query.p;
|
||||
let initialPage = 1;
|
||||
@@ -747,4 +867,108 @@ onMounted(() => {
|
||||
.cell-ip-text:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: #ffffff;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: 100vh;
|
||||
padding: 20px 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
transform: translateX(0);
|
||||
animation: drawer-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.modal-btn.primary {
|
||||
background-color: #0969da;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-btn.secondary {
|
||||
background-color: #f6f8fa;
|
||||
border-color: #d0d7de;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d7de;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #0969da;
|
||||
box-shadow: 0 0 0 1px rgba(9, 105, 218, 0.2);
|
||||
}
|
||||
|
||||
@keyframes drawer-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
94
cwd-api/src/api/admin/updateComment.ts
Normal file
94
cwd-api/src/api/admin/updateComment.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Context } from 'hono';
|
||||
import { Bindings } from '../../bindings';
|
||||
import { checkContent } from '../public/postComment';
|
||||
import { marked } from 'marked';
|
||||
import xss from 'xss';
|
||||
|
||||
export const updateComment = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
let body: any;
|
||||
try {
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
return c.json({ message: 'Invalid JSON body' }, 400);
|
||||
}
|
||||
|
||||
const rawId = body?.id;
|
||||
const id =
|
||||
typeof rawId === 'number'
|
||||
? rawId
|
||||
: typeof rawId === 'string' && rawId.trim()
|
||||
? Number.parseInt(rawId.trim(), 10)
|
||||
: NaN;
|
||||
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return c.json({ message: 'Missing or invalid id' }, 400);
|
||||
}
|
||||
|
||||
const existing = await c.env.CWD_DB.prepare(
|
||||
'SELECT id, status FROM Comment WHERE id = ?'
|
||||
)
|
||||
.bind(id)
|
||||
.first<{ id: number; status: string }>();
|
||||
|
||||
if (!existing) {
|
||||
return c.json({ message: 'Comment not found' }, 404);
|
||||
}
|
||||
|
||||
const rawName = typeof body.name === 'string' ? body.name : '';
|
||||
const rawEmail = typeof body.email === 'string' ? body.email : '';
|
||||
const rawUrl = typeof body.url === 'string' ? body.url : '';
|
||||
const rawStatus = typeof body.status === 'string' ? body.status : existing.status;
|
||||
|
||||
const contentSource =
|
||||
typeof body.content === 'string'
|
||||
? body.content
|
||||
: typeof body.contentText === 'string'
|
||||
? body.contentText
|
||||
: '';
|
||||
|
||||
const name = rawName.trim();
|
||||
const email = rawEmail.trim();
|
||||
const url = rawUrl.trim() || null;
|
||||
const status = rawStatus.trim();
|
||||
|
||||
if (!name) {
|
||||
return c.json({ message: '昵称不能为空' }, 400);
|
||||
}
|
||||
if (!email) {
|
||||
return c.json({ message: '邮箱不能为空' }, 400);
|
||||
}
|
||||
|
||||
const cleanedContent = checkContent(contentSource);
|
||||
const contentText = cleanedContent;
|
||||
|
||||
if (!contentText) {
|
||||
return c.json({ message: '评论内容不能为空' }, 400);
|
||||
}
|
||||
|
||||
const html = await marked.parse(cleanedContent, { async: true });
|
||||
const contentHtml = xss(html, {
|
||||
whiteList: {
|
||||
...xss.whiteList,
|
||||
code: ['class'],
|
||||
span: ['class', 'style'],
|
||||
pre: ['class'],
|
||||
div: ['class', 'style'],
|
||||
img: ['src', 'alt', 'title', 'width', 'height', 'style']
|
||||
}
|
||||
});
|
||||
|
||||
const { success } = await c.env.CWD_DB.prepare(
|
||||
'UPDATE Comment SET name = ?, email = ?, url = ?, content_text = ?, content_html = ?, status = ? WHERE id = ?'
|
||||
)
|
||||
.bind(name, email, url, contentText, contentHtml, status, id)
|
||||
.run();
|
||||
|
||||
if (!success) {
|
||||
return c.json({ message: 'Update failed' }, 500);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: `Comment updated, id: ${id}.`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { listComments } from './api/admin/listComments';
|
||||
import { exportComments } from './api/admin/exportComments';
|
||||
import { importComments } from './api/admin/importComments';
|
||||
import { updateStatus } from './api/admin/updateStatus';
|
||||
import { updateComment } from './api/admin/updateComment';
|
||||
import { getAdminEmail } from './api/admin/getAdminEmail';
|
||||
import { setAdminEmail } from './api/admin/setAdminEmail';
|
||||
import { testEmail } from './api/admin/testEmail';
|
||||
@@ -237,6 +238,7 @@ app.get('/admin/comments/list', listComments);
|
||||
app.get('/admin/comments/export', exportComments);
|
||||
app.post('/admin/comments/import', importComments);
|
||||
app.put('/admin/comments/status', updateStatus);
|
||||
app.put('/admin/comments/update', updateComment);
|
||||
app.get('/admin/stats/comments', getStats);
|
||||
app.get('/admin/settings/email', getAdminEmail);
|
||||
app.put('/admin/settings/email', setAdminEmail);
|
||||
|
||||
Reference in New Issue
Block a user