评论区改用公共 Markdown 编辑器(不含隐藏内容),并忽略 GORM 未命中日志噪音。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ import CommentsUnsupported from "@/components/CommentsUnsupported";
|
||||
import Avatar from "./Avatar";
|
||||
import Pagination from "./Pagination";
|
||||
import MarkdownBodyClient from "@/components/MarkdownBodyClient";
|
||||
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||
import DeleteCommentModal, { type DeleteCommentPayload } from "@/components/DeleteCommentModal";
|
||||
import CommentEditHistoryModal from "@/components/CommentEditHistoryModal";
|
||||
@@ -127,7 +128,6 @@ export default function CommentSection({
|
||||
const [historyCommentId, setHistoryCommentId] = useState<number | null>(null);
|
||||
// 折叠状态:哪些楼层展开了子回复(默认全部折叠,SSR 初始为收起)
|
||||
const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set());
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!commentsOpen) {
|
||||
@@ -142,7 +142,7 @@ export default function CommentSection({
|
||||
const focusComposer = () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash !== "#comment-input" && hash !== "#comments") return;
|
||||
const el = textareaRef.current ?? document.getElementById("comment-input");
|
||||
const el = document.getElementById("comment-input");
|
||||
if (!(el instanceof HTMLTextAreaElement)) return;
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
window.setTimeout(() => el.focus({ preventScroll: true }), 280);
|
||||
@@ -152,9 +152,8 @@ export default function CommentSection({
|
||||
return () => window.removeEventListener("hashchange", focusComposer);
|
||||
}, [user, commentsOpen]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!commentsOpen || !content.trim()) return;
|
||||
const submitFloor = async () => {
|
||||
if (!commentsOpen || !content.trim() || submitting) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await apiCreateComment(postId, content);
|
||||
@@ -179,9 +178,13 @@ export default function CommentSection({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReply = async (e: React.FormEvent) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!commentsOpen || !replyTarget || !replyContent.trim()) return;
|
||||
void submitFloor();
|
||||
};
|
||||
|
||||
const submitReply = async () => {
|
||||
if (!commentsOpen || !replyTarget || !replyContent.trim() || submitting) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await apiCreateComment(postId, replyContent, replyTarget.id);
|
||||
@@ -207,6 +210,11 @@ export default function CommentSection({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReply = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
void submitReply();
|
||||
};
|
||||
|
||||
const deleteTarget = deleteTargetId != null ? findComment(comments, deleteTargetId) : null;
|
||||
const deleteMode: "self" | "staff" =
|
||||
deleteTarget && user && user.id === (deleteTarget.user?.id ?? deleteTarget.user_id)
|
||||
@@ -373,7 +381,7 @@ export default function CommentSection({
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (editingId == null || !editContent.trim()) return;
|
||||
if (editingId == null || !editContent.trim() || savingEdit) return;
|
||||
setSavingEdit(true);
|
||||
try {
|
||||
const res = await apiUpdateComment(postId, editingId, editContent);
|
||||
@@ -394,14 +402,18 @@ export default function CommentSection({
|
||||
const renderContentOrEditor = (c: CommentNode) => {
|
||||
if (editingId === c.id) {
|
||||
return (
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
<div className="cmt-md-editor mt-1">
|
||||
<MarkdownEditor
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
rows={4}
|
||||
className="input w-full text-sm"
|
||||
onChange={setEditContent}
|
||||
rows={5}
|
||||
disabled={savingEdit}
|
||||
autoFocus
|
||||
allowHide={false}
|
||||
placeholder="编辑评论…"
|
||||
onSubmitShortcut={() => {
|
||||
void handleSaveEdit();
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
@@ -452,38 +464,34 @@ export default function CommentSection({
|
||||
<div className="text-[12px] mb-2" style={{ color: "var(--ink-3)" }}>
|
||||
回复 <span style={{ color: "var(--accent)" }}>@{replyTarget.nickname}</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
rows={2}
|
||||
autoFocus
|
||||
className="field"
|
||||
aria-label={`回复 ${replyTarget.nickname}`}
|
||||
placeholder="友善回复..."
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 mt-2">
|
||||
<span className="meta text-[11.5px]">支持 Markdown · Ctrl + Enter 发送</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
onClick={() => {
|
||||
setReplyTarget(null);
|
||||
setReplyContent("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" disabled={submitting || !replyContent.trim()} className="btn btn-primary btn-sm">
|
||||
{submitting ? "发送中..." : "回复"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="cmt-md-editor">
|
||||
<MarkdownEditor
|
||||
value={replyContent}
|
||||
onChange={setReplyContent}
|
||||
rows={4}
|
||||
disabled={submitting}
|
||||
autoFocus
|
||||
allowHide={false}
|
||||
placeholder={`友善回复 @${replyTarget.nickname}…`}
|
||||
onSubmitShortcut={() => {
|
||||
void submitReply();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
onClick={() => {
|
||||
setReplyTarget(null);
|
||||
setReplyContent("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" disabled={submitting || !replyContent.trim()} className="btn btn-primary btn-sm">
|
||||
{submitting ? "发送中..." : "回复"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -1066,24 +1074,21 @@ export default function CommentSection({
|
||||
|
||||
{user ? (
|
||||
<form onSubmit={handleSubmit} className="mb-8">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
id="comment-input"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder="友善发言,发表一个新楼层..."
|
||||
rows={3}
|
||||
className="field scroll-mt-28"
|
||||
aria-label="写评论"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 mt-2.5">
|
||||
<span className="meta text-[11.5px]">支持 Markdown · Ctrl + Enter 发送</span>
|
||||
<div className="cmt-md-editor cmt-md-editor-floor scroll-mt-28">
|
||||
<MarkdownEditor
|
||||
id="comment-input"
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
rows={6}
|
||||
disabled={submitting}
|
||||
allowHide={false}
|
||||
placeholder="友善发言,发表一个新楼层…"
|
||||
onSubmitShortcut={() => {
|
||||
void submitFloor();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-3 mt-2.5">
|
||||
<button type="submit" disabled={submitting || !content.trim()} className="btn btn-primary">
|
||||
{submitting ? "发送中..." : "发表评论"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user