diff --git a/public/img/icon/like-active.svg b/public/img/icon/like-active.svg
new file mode 100644
index 0000000..f78158f
--- /dev/null
+++ b/public/img/icon/like-active.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icon/like.svg b/public/img/icon/like.svg
new file mode 100644
index 0000000..5127207
--- /dev/null
+++ b/public/img/icon/like.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/app/globals.css b/src/app/globals.css
index 7fdd800..78882c0 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -26,6 +26,10 @@ input {
outline: none;
}
+textarea {
+ outline: none;
+}
+
@layer utilities {
.text-balance {
text-wrap: balance;
diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx
index b0f322c..20dff32 100644
--- a/src/components/Avatar.tsx
+++ b/src/components/Avatar.tsx
@@ -9,7 +9,7 @@ interface Props {
const Avatar = ({ src, alt = 'avatar', size, className = '' }: Props) => {
return (
-
+
);
diff --git a/src/components/Comment/ButtonBar.tsx b/src/components/Comment/ButtonBar.tsx
new file mode 100644
index 0000000..8e3cc28
--- /dev/null
+++ b/src/components/Comment/ButtonBar.tsx
@@ -0,0 +1,66 @@
+import { useEffect, useState } from 'react';
+
+interface Props {
+ commentCount: number;
+ thumbupCount: number;
+ haveThumbup: boolean;
+ className?: string;
+ onShowInput: () => void;
+ onShowAll: () => Promise
;
+ onThumbup: () => Promise;
+}
+export default function ButtonBar({
+ commentCount,
+ thumbupCount,
+ haveThumbup,
+ className,
+ onShowAll,
+ onShowInput,
+ onThumbup,
+}: Props) {
+ const [showAll, setShowAll] = useState(false);
+ const [thumbup, setThumbup] = useState(false);
+
+ const handleShowAll = async () => {
+ const res = await onShowAll();
+ setShowAll(res);
+ };
+
+ const handleThumbup = async () => {
+ const res = await onThumbup();
+ if (res) setThumbup(!thumbup);
+ };
+
+ useEffect(() => {
+ setThumbup(haveThumbup);
+ }, []);
+
+ return (
+
+ {/* 回复 */}
+
+ 回复
+
+ {/* 展开回复 */}
+ {commentCount > 1 && !showAll && (
+
+ 展开{commentCount}条回复
+
+ )}
+ {/* 点赞 */}
+
+ {thumbupCount > 0 && (
+
+ {thumbupCount}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx
index 1d5e76e..8b56c23 100644
--- a/src/components/Comment/Comment.tsx
+++ b/src/components/Comment/Comment.tsx
@@ -1,20 +1,56 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
import { CommentForm, CommentHeader, CommentList } from '@/components';
+import { apiGetComment, apiCommentSave } from '@/services';
+
+interface Props {
+ journalId: string;
+ className: string;
+ totalCommentReply: string;
+}
+
+export default function Comment({ journalId, className, totalCommentReply }: Props) {
+ const [commentType, setCommentType] = useState<'hot' | 'new'>('hot');
+ const [commentList, setCommentList] = useState([]);
+ const [page, setPage] = useState(1);
+
+ // 切换热门/最新
+ const handleChangeType = async (type: 'hot' | 'new') => {
+ setCommentType(type);
+ setPage(1);
+ };
+
+ // 发表评论
+ const handleSubmit = async (content: string) => {
+ const res = await apiCommentSave({ journalId, parentId: '', content });
+ return res.code === 200;
+ };
+
+ useEffect(() => {
+ const getCommentList = async () => {
+ if (page === 1) setCommentList([]);
+ const res = await apiGetComment({ type: commentType, journalId, page, size: 3 });
+ if (res.code === 200 && res.data?.rows.length) {
+ setCommentList([...commentList, ...res.data.rows]);
+ }
+ };
+
+ getCommentList();
+ }, [commentType, page]);
-export default function Comment({
- commentList,
- className,
- totalCommentReply,
-}: {
- commentList: any;
- className?: string;
- totalCommentReply: number;
-}) {
return (
-
+
{!!totalCommentReply && (
<>
-
+
handleChangeType(type)}
+ clssName="mt-[25px]"
+ />
>
)}
diff --git a/src/components/Comment/CommentForm.tsx b/src/components/Comment/CommentForm.tsx
index 590a6a5..e915333 100644
--- a/src/components/Comment/CommentForm.tsx
+++ b/src/components/Comment/CommentForm.tsx
@@ -1,3 +1,42 @@
-export default function CommentItem() {
- return ;
+'use client';
+
+import { useState } from 'react';
+
+import { Button } from '@/components';
+
+interface Props {
+ onSubmit: (value: string) => Promise;
+}
+
+export default function CommentItem({ onSubmit }: Props) {
+ const [value, setValue] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async () => {
+ setLoading(true);
+ await onSubmit(value);
+ setLoading(false);
+ };
+
+ return (
+
+
+ );
}
diff --git a/src/components/Comment/CommentHeader.tsx b/src/components/Comment/CommentHeader.tsx
index 5336992..e0330d9 100644
--- a/src/components/Comment/CommentHeader.tsx
+++ b/src/components/Comment/CommentHeader.tsx
@@ -1,15 +1,35 @@
-export default function CommentHeader({ count }: { count: number }) {
+export default function CommentHeader({
+ type,
+ count,
+ clssName,
+ onChange,
+}: {
+ type: 'hot' | 'new';
+ count: string;
+ clssName: string;
+ onChange: (value: 'hot' | 'new') => void;
+}) {
return (
-
+
-
热门
-
|
-
最新
+
onChange('hot')}
+ >
+ 热门
+
+
|
+
onChange('new')}
+ >
+ 最新
+
);
diff --git a/src/components/Comment/CommentInput.tsx b/src/components/Comment/CommentInput.tsx
new file mode 100644
index 0000000..4884b4a
--- /dev/null
+++ b/src/components/Comment/CommentInput.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+/** 评论输入框 */
+import { useState, useRef, useMemo, useEffect } from 'react';
+
+import { Avatar, Button, Input } from '@/components';
+
+interface Props {
+ nickName: string;
+ avatar: string;
+ className?: string;
+ onSubmit: (value: string) => Promise
;
+}
+
+export default function CommentInput({ nickName, avatar, className, onSubmit }: Props) {
+ const inputRef = useRef(null);
+ const [value, setValue] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const btnDisabled = useMemo(() => {
+ return loading && !value;
+ }, [loading, value]);
+
+ const handleReplySubmit = async () => {
+ if (!value) return;
+ setLoading(true);
+ await onSubmit(value);
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ inputRef.current && inputRef.current.focus();
+ });
+
+ return (
+
+
+
+
setValue(e.target.value)}
+ className="w-[600px] h-[48px] px-[14px] rounded-[3px] bg-[#F2F3F7] text-[rgba(0,0,0,0.4)] text-[14px] leading-[19.6px]"
+ />
+
+
+
+ );
+}
diff --git a/src/components/Comment/CommentItem.tsx b/src/components/Comment/CommentItem.tsx
index 590a6a5..5ded39f 100644
--- a/src/components/Comment/CommentItem.tsx
+++ b/src/components/Comment/CommentItem.tsx
@@ -1,3 +1,102 @@
-export default function CommentItem() {
- return ;
+'use client';
+
+import { useState, useRef } from 'react';
+
+import ButtonBar from './ButtonBar';
+import CommentInput from './CommentInput';
+import SubCommentItem from './CommentSubItem';
+
+import { Avatar } from '@/components';
+import { apiCommentThumbup, apiCommentSave, apiGetSubComment } from '@/services';
+
+// 回复
+const handleReply = async (params: { content: string; journalId: string; parentId: string }) => {
+ await apiCommentSave(params);
+};
+
+export default function CommentItem({
+ _id,
+ avatar,
+ nickName,
+ publishTime,
+ location,
+ content,
+ haveThumbup,
+ thumbupCount,
+ commentCount,
+ topChildrenComment,
+ journalId,
+ className,
+}: Comment & { className?: string }) {
+ const inputRef = useRef(null);
+ const [showAll, setShowAll] = useState(false);
+ const [showInput, setShowInput] = useState(false);
+ const [subCommentList, setSubCommentList] = useState(topChildrenComment ? [topChildrenComment] : []);
+
+ // 展示全部评论
+ const handleShowAll = async () => {
+ setShowAll(true);
+ const res = await apiGetSubComment({ parentId: _id, size: commentCount, page: 1 });
+ if (res.code === 200) setSubCommentList(res.data.rows);
+ return res.code === 200;
+ };
+
+ // 展示回复框
+ const handleShowInput = () => {
+ setShowInput(true);
+ inputRef.current && inputRef.current.focus();
+ };
+
+ // 提交回复
+ const handleReplySubmit = async (value: string) => {
+ const res = await apiCommentSave({ content: value, journalId: journalId, parentId: _id });
+ if (res.code === 200) setShowInput(false);
+ return res.code === 200;
+ };
+
+ // 点赞
+ const handleThumbup = async () => {
+ const res = await apiCommentThumbup(_id);
+ return res.code === 200;
+ };
+
+ return (
+
+ {/* 一级评论 */}
+
+
+
+
{nickName}
+
{`${publishTime} ${location}`}
+
{content}
+ {/* 子评论 */}
+
+ {subCommentList.length > 0 &&
+ subCommentList.map((subComment) => (
+
+ ))}
+
+
+
+ {/* 按钮 */}
+
+ {/* 回复 */}
+ {showInput && (
+
+ )}
+
+ );
}
diff --git a/src/components/Comment/CommentList.tsx b/src/components/Comment/CommentList.tsx
index a1ba10e..690817c 100644
--- a/src/components/Comment/CommentList.tsx
+++ b/src/components/Comment/CommentList.tsx
@@ -1,3 +1,10 @@
+import CommentItem from './CommentItem';
+
export default function CommentList({ commentList }: { commentList: Comment[] }) {
- return ;
+ return (
+
+ {commentList.length > 0 &&
+ commentList.map((comment) => )}
+
+ );
}
diff --git a/src/components/Comment/CommentSubInput.tsx b/src/components/Comment/CommentSubInput.tsx
new file mode 100644
index 0000000..83aa37f
--- /dev/null
+++ b/src/components/Comment/CommentSubInput.tsx
@@ -0,0 +1,10 @@
+/** 子评论输入框 */
+import { Avatar } from '@/components';
+
+export default function CommentSubInput() {
+ return (
+
+ );
+}
diff --git a/src/components/Comment/CommentSubItem.tsx b/src/components/Comment/CommentSubItem.tsx
new file mode 100644
index 0000000..f53c88f
--- /dev/null
+++ b/src/components/Comment/CommentSubItem.tsx
@@ -0,0 +1,26 @@
+import { Avatar } from '@/components';
+
+export default function SubCommentItem({
+ avatar,
+ nickName,
+ publishTime,
+ location,
+ content,
+ className,
+}: Comment & { className?: string }) {
+ return (
+
+
+
+
+
+ {`${nickName}: `}
+ {content}
+ {/* 归属地 */}
+
+
{`${publishTime} ${location}`}
+
+
+
+ );
+}
diff --git a/src/components/Header/HeaderAvatar.tsx b/src/components/Header/HeaderAvatar.tsx
index 39e3b3f..170d042 100644
--- a/src/components/Header/HeaderAvatar.tsx
+++ b/src/components/Header/HeaderAvatar.tsx
@@ -15,10 +15,9 @@ export default function HeaderAvatar({ className }: { className?: string }) {
return (
- {/* 已登录 展示头像 */}
- {!!userInfo.id &&
}
- {/* 未登录 展示按钮 */}
- {!userInfo.id && (
+ {!!userInfo.id ? (
+
+ ) : (