From b66de3129d7f5cf691c3e54980577f59bc15e279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E8=B6=A3=E4=BF=9D=E7=BD=97?= Date: Thu, 30 Jul 2026 00:59:09 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=96=B0=E5=A2=9E=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=BB=86=E5=8C=96=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=EF=BC=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app.css | 16 ++ app/components/layout/header.tsx | 6 +- app/components/shared/comment-form/index.tsx | 178 ++++++++++++++++++ app/components/shared/comment-item/index.tsx | 78 ++++++++ app/components/shared/comment/index.tsx | 188 +++++++++++++++++++ app/components/ui/notice/index.tsx | 14 +- app/components/ui/notice/utils.ts | 1 + app/composables/use-is-login.ts | 2 + app/routes/gallery.tsx | 6 +- app/routes/index.tsx | 4 +- app/routes/note-detail.tsx | 58 +++++- app/routes/note.tsx | 4 +- app/routes/page.tsx | 4 +- app/types/api.comment.d.ts | 43 +++++ app/types/api.note.d.ts | 2 +- app/utils/comment.ts | 27 +++ package.json | 1 + pnpm-lock.yaml | 12 ++ 18 files changed, 630 insertions(+), 14 deletions(-) create mode 100644 app/components/shared/comment-form/index.tsx create mode 100644 app/components/shared/comment-item/index.tsx create mode 100644 app/components/shared/comment/index.tsx create mode 100644 app/composables/use-is-login.ts create mode 100644 app/types/api.comment.d.ts create mode 100644 app/utils/comment.ts diff --git a/app/app.css b/app/app.css index adb54cd..f3aa470 100644 --- a/app/app.css +++ b/app/app.css @@ -8,6 +8,7 @@ --animate-fade-out: fade-out 0.3s both; --animate-fade-in-left: fade-in-left 0.3s backwards; --animate-fade-off-right: fade-off-right 0.3s forwards; + --animate-scale: scale 0.3s 1s; @keyframes spinner-bar { 0% { @@ -57,6 +58,21 @@ transform: translateX(1.5rem); } } + + @keyframes scale { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } + } } @media screen and (max-width: 639px) { diff --git a/app/components/layout/header.tsx b/app/components/layout/header.tsx index 0dab02f..a88554d 100644 --- a/app/components/layout/header.tsx +++ b/app/components/layout/header.tsx @@ -2,6 +2,8 @@ import { NavLink } from "react-router"; import { Feed, GitHub, QQ } from "../ui/icons"; import { clsn } from "~/utils"; +const { APP_APIURL } = import.meta.env; + const navItems = [ { name: "首页", to: "/" }, { name: "日记", to: "/note" }, @@ -34,13 +36,13 @@ function Header() { ))} diff --git a/app/components/shared/comment-form/index.tsx b/app/components/shared/comment-form/index.tsx new file mode 100644 index 0000000..48349ce --- /dev/null +++ b/app/components/shared/comment-form/index.tsx @@ -0,0 +1,178 @@ +import { useState, type ChangeEvent, type SubmitEvent } from "react"; +import { RiLoader5Fill } from "@remixicon/react"; + +import { add } from "~/components/ui/notice/utils"; +import { useIsLogin } from "~/composables/use-is-login"; +import { clsn } from "~/utils"; +import { getAuthorInfo, setAuthorInfo } from "~/utils/comment"; + +export interface CommentFormSubmitValues { + name: string; + email: string; + site: string; + content: string; + root?: string; + parent?: string; +} + +interface CommentFormProps { + className?: string; + parent?: string; + root?: string; + onSubmit: (values: CommentFormSubmitValues) => Promise; +} + +export default function CommentForm({ className, parent, root, onSubmit }: CommentFormProps) { + const isLogin = useIsLogin(); + const [loading, setLoading] = useState(false); + const [input, setInput] = useState(() => ({ + ...getAuthorInfo(), + content: "", + })); + + const onChange = (ev: ChangeEvent) => { + const { name, value } = ev.target; + setInput((prev) => ({ ...prev, [name]: value })); + }; + + const onFormSubmit = (ev: SubmitEvent) => { + ev.preventDefault(); + + if (!isLogin && (!input.name || !input.email || !input.content)) { + add({ + title: "评论失败", + type: "error", + content: "缺少必填项", + }); + + return; + } + + if (!input.content) { + add({ + title: "评论失败", + type: "error", + content: "缺少必填项", + }); + + return; + } + + setAuthorInfo({ + name: input.name, + email: input.email, + site: input.site, + }); + + setLoading(true); + + onSubmit({ + ...input, + root, + parent, + }).then(() => { + setInput((prev) => ({ ...prev, content: "" })); + setLoading(false); + }).catch(() => { + setLoading(false); + }); + }; + + const inputClass = "w-full px-3 py-2.5 rounded-xl bg-white border-4 border-gray-200 focus:border-pink-400 outline-none transition-colors placeholder:text-neutral-400"; + const labelClass = "block text-sm font-medium mb-2 text-neutral-500"; + + if (isLogin) { + return ( +
+
+

欢迎回来!

+
+
+