feat(评论管理): 添加编辑评论功能

在评论管理页面添加编辑功能,允许管理员修改评论的昵称、邮箱、网址、内容和状态
This commit is contained in:
anghunk
2026-01-21 17:25:49 +08:00
parent ce7c306acb
commit 27afa2db4f
4 changed files with 338 additions and 0 deletions

View File

@@ -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');
}

View File

@@ -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>