add
This commit is contained in:
parent
20e9d43c48
commit
bb2aa599ba
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,6 @@ export function PostDetailProvider({
|
||||||
},
|
},
|
||||||
{ enabled: Boolean(editId) }
|
{ enabled: Boolean(editId) }
|
||||||
);
|
);
|
||||||
// const {}:{} =(
|
|
||||||
// api.post.fin as any
|
|
||||||
// )
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostDetailContext.Provider
|
<PostDetailContext.Provider
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
// 复合索引
|
// 复合索引
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue