diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts index 826f8ac..ce7ba9c 100644 --- a/cwd-admin/src/api/admin.ts +++ b/cwd-admin/src/api/admin.ts @@ -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 { return get('/admin/settings/email'); } diff --git a/cwd-admin/src/views/CommentsView.vue b/cwd-admin/src/views/CommentsView.vue index ed5f85e..604d88a 100644 --- a/cwd-admin/src/views/CommentsView.vue +++ b/cwd-admin/src/views/CommentsView.vue @@ -103,6 +103,7 @@ > 拒绝 + + + + + @@ -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([]); @@ -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); + } +} diff --git a/cwd-api/src/api/admin/updateComment.ts b/cwd-api/src/api/admin/updateComment.ts new file mode 100644 index 0000000..e121235 --- /dev/null +++ b/cwd-api/src/api/admin/updateComment.ts @@ -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}.` + }); +}; + diff --git a/cwd-api/src/index.ts b/cwd-api/src/index.ts index df554c2..da393e1 100644 --- a/cwd-api/src/index.ts +++ b/cwd-api/src/index.ts @@ -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);