This commit is contained in:
ditiqi 2025-01-24 11:39:51 +08:00
parent 20e9d43c48
commit bb2aa599ba
7 changed files with 178 additions and 202 deletions

View File

@ -1,69 +1,48 @@
import { createContext, useContext, ReactNode, useEffect } from "react"; import { createContext, useContext, ReactNode } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form"; import { Form, FormInstance } from "antd";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { api, usePost } from "@nice/client"; import { api, usePost } from "@nice/client";
// import { PostDto, PostLevel, PostStatus } from "@nice/common";
// import { api, usePost } from "@nice/client";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Post, PostType } from "@nice/common"; import { PostType } from "@nice/common";
// 定义帖子表单验证 Schema
const letterSchema = z.object({ export interface LetterFormData {
title: z.string().min(1, "标题不能为空"), title: string;
content: z.string().min(1, "内容不能为空"), content: string;
resources: z.array(z.string()).nullish(), resources?: string[];
isPublic: z.boolean().nullish(), isPublic?: boolean;
signature: z.string().nullish(), signature?: string;
meta: z meta: {
.object({ tags: string[];
tags: z.array(z.string()).default([]), signature?: string;
signature: z.string().nullish(), };
}) }
.default({
tags: [],
signature: null,
}),
});
// 定义课程表单验证 Schema
export type LetterFormData = z.infer<typeof letterSchema>;
interface LetterEditorContextType { interface LetterEditorContextType {
onSubmit: SubmitHandler<LetterFormData>; onSubmit: (values: LetterFormData) => Promise<void>;
receiverId?: string; receiverId?: string;
termId?: string; termId?: string;
part?: string; form: FormInstance<LetterFormData>;
// course?: PostDto;
} }
interface LetterFormProviderProps { interface LetterFormProviderProps {
children: ReactNode; children: ReactNode;
receiverId?: string; receiverId?: string;
termId?: string; termId?: string;
part?: string;
} }
const LetterEditorContext = createContext<LetterEditorContextType | null>(null); const LetterEditorContext = createContext<LetterEditorContextType | null>(null);
export function LetterFormProvider({ export function LetterFormProvider({
children, children,
receiverId, receiverId,
termId, termId,
// editId,
}: LetterFormProviderProps) { }: LetterFormProviderProps) {
const { create } = usePost(); const { create } = usePost();
const navigate = useNavigate(); const navigate = useNavigate();
const methods = useForm<LetterFormData>({ const [form] = Form.useForm<LetterFormData>();
resolver: zodResolver(letterSchema),
defaultValues: { const onSubmit = async (data: LetterFormData) => {
resources: [],
meta: {
tags: [],
},
},
});
const onSubmit: SubmitHandler<LetterFormData> = async (
data: LetterFormData
) => {
try { try {
console.log("data", data);
const result = await create.mutateAsync({ const result = await create.mutateAsync({
data: { data: {
type: PostType.POST, type: PostType.POST,
@ -73,6 +52,7 @@ export function LetterFormProvider({
id, id,
})), })),
}, },
isPublic: data?.isPublic,
...data, ...data,
resources: data.resources?.length resources: data.resources?.length
? { ? {
@ -83,23 +63,28 @@ export function LetterFormProvider({
: undefined, : undefined,
}, },
}); });
navigate(`/course/${result.id}/detail`, { replace: true }); navigate(`/${result.id}/detail`, { replace: true });
toast.success("发送成功!"); toast.success("发送成功!");
form.resetFields();
methods.reset(data);
} catch (error) { } catch (error) {
console.error("Error submitting form:", error); console.error("Error submitting form:", error);
toast.error("操作失败,请重试!"); toast.error("操作失败,请重试!");
} }
}; };
return ( return (
<LetterEditorContext.Provider <LetterEditorContext.Provider
value={{ value={{
onSubmit, onSubmit,
receiverId, receiverId,
termId, termId,
form,
}}> }}>
<FormProvider {...methods}>{children}</FormProvider> <Form<LetterFormData>
form={form}
initialValues={{ meta: { tags: [] } }}>
{children}
</Form>
</LetterEditorContext.Provider> </LetterEditorContext.Provider>
); );
} }

View File

@ -1,29 +1,18 @@
import { useFormContext } from "react-hook-form"; import { Form, Input, Button, Checkbox, Select } from "antd";
import { motion } from "framer-motion"; import { useState } from "react";
import { import { useLetterEditor } from "../context/LetterEditorContext";
LetterFormData,
useLetterEditor,
} from "../context/LetterEditorContext";
import { FormQuillInput } from "@web/src/components/common/form/FormQuillInput";
import { api } from "@nice/client"; import { api } from "@nice/client";
import { import {
UserIcon, UserOutlined,
FolderIcon, FolderOutlined,
HashtagIcon, TagOutlined,
DocumentTextIcon, FileTextOutlined,
} from "@heroicons/react/24/outline"; } from "@ant-design/icons";
import FileUploader from "@web/src/components/common/uploader/FileUploader"; import FileUploader from "@web/src/components/common/uploader/FileUploader";
import { FormTags } from "@web/src/components/common/form/FormTags"; import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
import { FormSignature } from "@web/src/components/common/form/FormSignature";
import { FormCheckbox } from "@web/src/components/common/form/FormCheckbox";
export function LetterBasicForm() { export function LetterBasicForm() {
const { const { onSubmit, receiverId, termId, form } = useLetterEditor();
handleSubmit,
getValues,
formState: { errors },
} = useFormContext<LetterFormData>();
const { onSubmit, receiverId, termId } = useLetterEditor();
const { data: receiver } = api.staff.findFirst.useQuery( const { data: receiver } = api.staff.findFirst.useQuery(
{ {
where: { where: {
@ -41,134 +30,137 @@ export function LetterBasicForm() {
{ enabled: !!termId } { enabled: !!termId }
); );
const formControls = { const handleFinish = async (values: any) => {
hidden: { opacity: 0, y: 20 }, await onSubmit(values);
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.2,
duration: 0.5,
},
}),
}; };
return ( return (
<motion.form <div className="w-full space-y-6 p-8">
className="w-full space-y-6 p-8 " <Form
initial={{ opacity: 0, scale: 0.95 }} form={form}
animate={{ opacity: 1, scale: 1 }} onFinish={handleFinish}
transition={{ duration: 0.5 }}> initialValues={{ meta: { tags: [] }, isPublic: true }}>
{/* 收件人和板块信息行 */} {/* 收件人和板块信息行 */}
<div className="flex justify-start items-center gap-8"> <div className="flex justify-start items-center gap-8 ">
<motion.div <div className="flex items-center font-semibold text-[#00308F]">
custom={0} <UserOutlined className="w-5 h-5 mr-2 text-[#00308F]" />
initial="hidden" <div>{receiver?.showname}</div>
animate="visible" </div>
variants={formControls}
className="flex items-center text-lg font-semibold text-[#00308F]">
<UserIcon className="w-5 h-5 mr-2 text-[#00308F]" />
<div>{receiver?.showname}</div>
</motion.div>
<motion.div <div className="flex items-center font-semibold text-[#00308F]">
custom={1} <FolderOutlined className="w-5 h-5 mr-2 text-[#00308F]" />
initial="hidden" <div>{term?.name}</div>
animate="visible" </div>
variants={formControls} </div>
className="flex items-center text-lg font-semibold text-[#00308F]">
<FolderIcon className="w-5 h-5 mr-2 text-[#00308F]" /> {/* 主题输入框 */}
<div>{term?.name}</div> <div className="space-y-2 mt-4">
</motion.div> <Form.Item
</div> required={false} //不显示星号
{/* 主题输入框 */} label={
<motion.div <span className="block mb-1">
custom={2} <TagOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
initial="hidden"
animate="visible" <span className="text-gray-400"></span>
variants={formControls}> </span>
<label className="block text-sm font-medium text-gray-700 mb-2"> }
<HashtagIcon className="w-5 h-5 inline mr-2 text-[#00308F]" /> name="title"
rules={[{ required: true, message: "请输入标题" }]}
</label> labelCol={{ span: 24 }}
{/* <FormInput wrapperCol={{ span: 24 }}>
maxLength={20} <Input maxLength={20} placeholder="请输入标题" />
name="title" </Form.Item>
placeholder="请输入主题" </div>
/> */}
</motion.div> {/* 标签输入 */}
{/* 标签输入 */} <div className="space-y-2">
<motion.div <Form.Item
custom={4} label={
initial="hidden" <span className="block mb-1">
animate="visible" <TagOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
variants={formControls}
className="space-y-2"> </span>
<label className="block text-sm font-medium text-gray-700 mb-2"> }
<HashtagIcon className="w-5 h-5 inline mr-2 text-[#00308F]" /> name={["meta", "tags"]}
labelCol={{ span: 24 }}
</label> wrapperCol={{ span: 24 }}>
<FormTags <Select
name="meta.tags" mode="tags"
placeholder="输入标签后按回车添加" placeholder="输入标签后按回车添加"
maxTags={20} value={form.getFieldValue(["meta", "tags"]) || []}
/> onChange={(value) => {
</motion.div> form.setFieldValue(["meta", "tags"], value);
{/* 内容输入框 */} }}
<motion.div tokenSeparators={[",", " "]}
custom={3} className="w-full"
initial="hidden" dropdownStyle={{ display: "none" }}
animate="visible" style={{
variants={formControls}> backgroundColor: "#f0f4ff",
<label className="block text-sm font-medium text-gray-700 mb-2"> borderColor: "#00308F",
<DocumentTextIcon className="w-5 h-5 inline mr-2 text-[#00308F]" /> borderRadius: "6px",
}}
</label> />
<FormQuillInput </Form.Item>
maxLength={400} </div>
name="content"
placeholder="请输入内容" {/* 内容输入框 */}
/> <div className="space-y-2">
</motion.div> <Form.Item
<FileUploader></FileUploader> label={
<motion.div className="flex items-center justify-end gap-8 border-t border-gray-100 pt-2"> <span className="block mb-1">
<motion.div <FileTextOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
custom={3}
initial="hidden" <span className="text-gray-400"></span>
animate="visible" </span>
variants={formControls} }
className="flex justify-end"> name="content"
<FormCheckbox rules={[{ required: true, message: "请输入内容" }]}
required={false} //不显示星号
labelCol={{ span: 24 }}
wrapperCol={{ span: 24 }}>
<div className="relative rounded-lg border border-slate-200 bg-white shadow-sm">
<QuillEditor
maxLength={400}
placeholder="请输入内容"
minRows={6}
maxRows={12}
onChange={(content) =>
form.setFieldValue("content", content)
}
/>
</div>
</Form.Item>
</div>
{/* <FileUploader /> */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-end gap-4 border-t border-gray-100 pt-4">
<Form.Item
name="isPublic" name="isPublic"
label="是否公开" valuePropName="checked"
defaultChecked className="mb-0"
/> initialValue={true}>
</motion.div> <Checkbox></Checkbox>
{/* </Form.Item>
<motion.div
custom={5}
initial="hidden"
animate="visible"
variants={formControls}
className="flex justify-end">
<FormSignature
name="meta.signature"
width="w-32"
placeholder="添加个性签名"
maxLength={20}
viewMode={false}
/>
</motion.div> */}
<motion.button <Button
onClick={handleSubmit(onSubmit)} type="primary"
className="px-4 py-2 bg-[#00308F] hover:bg-[#041E42] text-white font-bold rounded-lg onClick={() => form.submit()}
transform transition-all duration-200 hover:scale-105 focus:ring-2 focus:ring-[#00308F]/50" className="bg-[#00308F] hover:bg-[#041E42] w-full sm:w-auto"
whileHover={{ scale: 1.02 }} style={{
whileTap={{ scale: 0.98 }}> transform: "scale(1)",
transition: "all 0.2s",
</motion.button> }}
</motion.div> onMouseEnter={(e) => {
</motion.form> e.currentTarget.style.transform = "scale(1.02)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "scale(1)";
}}>
</Button>
</div>
</Form>
</div>
); );
} }

View File

@ -3,7 +3,7 @@ import { motion } from "framer-motion";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ChatBubbleLeftIcon, HeartIcon } from "@heroicons/react/24/outline"; import { ChatBubbleLeftIcon, HeartIcon } from "@heroicons/react/24/outline";
import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid"; import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid";
import { Avatar } from "@web/src/components/presentation/user/Avatar"; import { Avatar } from "antd";
import { useVisitor } from "@nice/client"; import { useVisitor } from "@nice/client";
import { useContext } from "react"; import { useContext } from "react";
import { PostDetailContext } from "./context/PostDetailContext"; import { PostDetailContext } from "./context/PostDetailContext";
@ -29,7 +29,7 @@ export default function PostCommentCard({
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to like post:', error); console.error("Failed to like post:", error);
} }
} }
} }
@ -41,9 +41,10 @@ export default function PostCommentCard({
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Avatar <Avatar
src={post.author?.avatar} src={post.author?.avatar}
name={post.author?.showname || "匿名用户"}
size={40} size={40}
/> >
{!post.author?.avatar && (post.author?.showname || "匿名用户")}
</Avatar>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div <div

View File

@ -54,7 +54,7 @@ export default function PostCommentEditor() {
placeholder="写下你的回复..." placeholder="写下你的回复..."
className="bg-transparent" className="bg-transparent"
theme="snow" theme="snow"
minRows={3} minRows={6}
maxRows={12} maxRows={12}
modules={{ modules={{
toolbar: [ toolbar: [

View File

@ -30,10 +30,7 @@ export function PostDetailProvider({
}, },
{ enabled: Boolean(editId) } { enabled: Boolean(editId) }
); );
// const {}:{} =(
// api.post.fin as any
// )
return ( return (
<PostDetailContext.Provider <PostDetailContext.Provider
value={{ value={{

View File

@ -209,7 +209,7 @@ model Post {
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型 parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型 children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
resources Resource[] // 附件列表 resources Resource[] // 附件列表
isPublic Boolean? @default(false) @map("is_public") isPublic Boolean? @default(true) @map("is_public")
meta Json? // 签名 和 IP 和 tags meta Json? // 签名 和 IP 和 tags
// 复合索引 // 复合索引

View File

@ -7,6 +7,7 @@ export const postDetailSelect: Prisma.PostSelect = {
content: true, content: true,
views: true, views: true,
likes: true, likes: true,
isPublic: true,
resources: true, resources: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,