This commit is contained in:
eric
2026-03-15 04:25:45 -05:00
parent df12c7af4a
commit bbaa38b3b3
32 changed files with 4312 additions and 152 deletions

View File

@@ -0,0 +1,179 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import {
buildCommunityAvatarText,
formatCommunityTime,
maskCommunityUserId,
type CommunityComment,
} from "./CommunityMemoCard";
import { renderCommunityMarkdown } from "./renderMarkdown";
import { emitCommunityChanged } from "./communityEvents";
import styles from "./community.module.css";
type CommunityCommentThreadProps = {
memoId: string;
userId: string;
memoOwnerId: string;
comments?: CommunityComment[];
onCommentsChanged?: (comments: CommunityComment[]) => void;
};
export function CommunityCommentThread({
memoId,
userId,
memoOwnerId,
comments = [],
onCommentsChanged,
}: CommunityCommentThreadProps) {
const [items, setItems] = useState<CommunityComment[]>(comments);
const [content, setContent] = useState("");
const [submitting, setSubmitting] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setItems(comments);
}, [comments]);
const syncComments = (nextItems: CommunityComment[]) => {
setItems(nextItems);
onCommentsChanged?.(nextItems);
};
const canSubmit = useMemo(() => Boolean(content.trim()) && !submitting, [content, submitting]);
const handleSubmit = async () => {
const trimmed = content.trim();
if (!trimmed || submitting) {
return;
}
setSubmitting(true);
setError(null);
try {
const response = await fetch(`/api/community/memos/${memoId}/comments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, content: trimmed }),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.comment) {
setError(data?.error || "回复发布失败,请稍后重试。");
return;
}
const nextItems = [...items, data.comment as CommunityComment];
syncComments(nextItems);
setContent("");
emitCommunityChanged();
} catch {
setError("网络异常,请稍后重试。");
} finally {
setSubmitting(false);
}
};
const handleDelete = async (commentId: string) => {
if (!window.confirm("确定删除这条评论吗?")) {
return;
}
setDeletingId(commentId);
setError(null);
try {
const response = await fetch(
`/api/community/comments/${commentId}?user_id=${encodeURIComponent(userId)}`,
{ method: "DELETE" }
);
const data = await response.json().catch(() => null);
if (!response.ok) {
setError(data?.error || "评论删除失败,请稍后重试。");
return;
}
const nextItems = items.filter((item) => item.id !== commentId);
syncComments(nextItems);
emitCommunityChanged();
} catch {
setError("网络异常,请稍后重试。");
} finally {
setDeletingId(null);
}
};
return (
<section className={styles.commentPanel}>
<span className={styles.commentEyebrow}>Discussion</span>
<h2 className={styles.commentTitle}></h2>
<p className={styles.commentHint}></p>
<div className={styles.commentForm}>
<textarea
className={styles.commentTextarea}
value={content}
onChange={(event) => setContent(event.target.value)}
placeholder="写下你的补充、问题或结论..."
maxLength={3000}
disabled={submitting}
/>
<div className={styles.commentFormFooter}>
<span className={styles.commentCount}>{items.length} </span>
<button
type="button"
className={styles.commentSubmit}
onClick={() => void handleSubmit()}
disabled={!canSubmit}
>
{submitting ? "发送中..." : "发送评论"}
</button>
</div>
</div>
{error ? <p className={styles.commentStatus}>{error}</p> : null}
{items.length ? (
<div className={styles.commentList}>
{items.map((comment) => {
const canDelete = comment.user_id === userId || memoOwnerId === userId;
return (
<article key={comment.id} className={styles.commentItem}>
<div className={styles.commentHeader}>
<div className={styles.memoIdentity}>
<div className={styles.memoAvatar}>{buildCommunityAvatarText(comment.user_id)}</div>
<div className={styles.commentMeta}>
<span className={styles.commentAuthor}>{maskCommunityUserId(comment.user_id)}</span>
<span className={styles.commentTime}>
{formatCommunityTime(comment.created || comment.updated)}
</span>
</div>
</div>
{canDelete ? (
<div className={styles.commentActions}>
<button
type="button"
className={styles.commentDeleteBtn}
onClick={() => void handleDelete(comment.id)}
disabled={deletingId === comment.id}
>
{deletingId === comment.id ? "删除中..." : "删除"}
</button>
</div>
) : null}
</div>
<div
className={styles.commentContent}
dangerouslySetInnerHTML={{ __html: renderCommunityMarkdown(comment.content) }}
/>
</article>
);
})}
</div>
) : (
<p className={styles.commentEmpty}></p>
)}
</section>
);
}