实现博客评论
This commit is contained in:
@@ -4,29 +4,69 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { BlogApi } from "@/lib/api";
|
||||
import { BlogComment } from "@/lib/types/blogComment";
|
||||
import { Send } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Send, Undo2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function BlogCommentTool({ blogId, onInsertComment }: { blogId: string, onInsertComment: (b: BlogComment) => void }) {
|
||||
interface BlogCommentToolProps {
|
||||
blogId: string;
|
||||
onInsertComment: (b: BlogComment) => void;
|
||||
replayTarget: BlogComment | null;
|
||||
handleClearReplayTarget: () => void;
|
||||
}
|
||||
|
||||
export function BlogCommentTool({ blogId, onInsertComment, replayTarget, handleClearReplayTarget }: BlogCommentToolProps) {
|
||||
const [comment, setComment] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (replayTarget && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
textareaRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'start'
|
||||
})
|
||||
}
|
||||
}, [replayTarget]);
|
||||
|
||||
const submit = async () => {
|
||||
if (comment.trim().length === 0) return;
|
||||
const res = await BlogApi.createComment(blogId, comment);
|
||||
const res = await BlogApi.createComment(blogId, comment, replayTarget ? replayTarget.id : undefined);
|
||||
if (res) {
|
||||
toast.success('发布成功');
|
||||
setComment('');
|
||||
onInsertComment(res);
|
||||
handleClearReplayTarget();
|
||||
}
|
||||
}
|
||||
|
||||
const getPlaceHolderText = () => {
|
||||
if (!replayTarget) return '评论';
|
||||
|
||||
let replayComment = replayTarget.content.trim();
|
||||
if (replayComment.length > 8) {
|
||||
replayComment = replayComment.slice(0, 8) + '...';
|
||||
}
|
||||
|
||||
const replayUser = replayTarget.user ? replayTarget.user.nickname : '匿名';
|
||||
|
||||
return `回复 ${replayUser} 的 ${replayComment}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 flex items-end gap-2">
|
||||
<Textarea placeholder="评论" onChange={v => setComment(v.target.value)} value={comment} />
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
placeholder={getPlaceHolderText()}
|
||||
onChange={v => setComment(v.target.value)}
|
||||
value={comment} />
|
||||
<Button variant='outline' size='icon' onClick={() => submit()} disabled={comment.trim().length === 0}>
|
||||
<Send />
|
||||
</Button>
|
||||
{replayTarget && <Button variant='outline' size='icon' onClick={() => handleClearReplayTarget()}>
|
||||
<Undo2 />
|
||||
</Button>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import useSWR from "swr";
|
||||
import { BlogCommentTool } from "./BlogCommentTool";
|
||||
import { BlogApi } from "@/lib/api";
|
||||
import { BlogComment } from "@/lib/types/blogComment";
|
||||
import { useState } from "react";
|
||||
|
||||
export function BlogComments({ blogId }: { blogId: string }) {
|
||||
const { data, isLoading, error, mutate } = useSWR(
|
||||
@@ -19,34 +20,42 @@ export function BlogComments({ blogId }: { blogId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const [replayTarget, setReplayTarget] = useState<BlogComment | null>(null);
|
||||
|
||||
return (
|
||||
data && <div className="" >
|
||||
<h1 className="px-2 border-l-4 border-zinc-300">评论 {data.length}</h1>
|
||||
<BlogCommentTool blogId={blogId} onInsertComment={insertComment} />
|
||||
<div className="flex flex-col gap-3">
|
||||
<BlogCommentTool
|
||||
blogId={blogId}
|
||||
onInsertComment={insertComment}
|
||||
replayTarget={replayTarget}
|
||||
handleClearReplayTarget={() => setReplayTarget(null)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
{
|
||||
data.filter(d => !d.parentId)
|
||||
.map(d => (
|
||||
<div key={d.id}>
|
||||
.map((d, dIndex) => (
|
||||
<div key={d.id} className="border-b border-zinc-300 py-2 last:border-none">
|
||||
<h1 className="text-zinc-500">{d.user ? d.user.nickname : '匿名'}</h1>
|
||||
<div>{d.content}</div>
|
||||
<div className="whitespace-pre-wrap break-all">{d.content}</div>
|
||||
<div className="text-xs text-zinc-500 flex gap-2">
|
||||
<p>{new Date(d.createdAt).toLocaleString()}</p>
|
||||
<p>未知</p>
|
||||
<p className="text-zinc-900 cursor-pointer">回复</p>
|
||||
<p className="text-zinc-900 cursor-pointer" onClick={() => setReplayTarget(d)}>回复</p>
|
||||
</div>
|
||||
{
|
||||
data.filter(c => c.parentId === d.id).length > 0 && (
|
||||
<div className="flex flex-col gap-3 ml-5 my-1">
|
||||
<div className="flex flex-col ml-5 my-1">
|
||||
{
|
||||
data.filter(c => c.parentId === d.id).map(c => (
|
||||
<div key={c.id}>
|
||||
<div key={c.id} className="border-b border-zinc-300 py-1 last:border-none">
|
||||
<h1 className="text-zinc-500">{c.user ? c.user.nickname : '匿名'}</h1>
|
||||
<div>{c.content}</div>
|
||||
<p className="text-xs text-zinc-500 flex gap-2">
|
||||
<div className="whitespace-pre-wrap break-all">{c.content}</div>
|
||||
<div className="text-xs text-zinc-500 flex gap-2">
|
||||
<p>{new Date().toLocaleString()}</p>
|
||||
<p>未知</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { BlogComment } from "@/lib/types/blogComment";
|
||||
import fetcher from "../fetcher";
|
||||
|
||||
export async function createComment(blogId: string, content: string) {
|
||||
export async function createComment(blogId: string, content: string, parentId?: string) {
|
||||
return fetcher<BlogComment>(`/api/blog/${blogId}/comment`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
parentId: parentId || null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user