From db2e3a044be98b3cbf96e02d3e9bdca173db9240 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sat, 25 Jan 2025 19:51:08 +0800 Subject: [PATCH] add --- apps/server/src/app.module.ts | 18 +- apps/server/src/main.ts | 3 +- apps/server/src/models/post/post.router.ts | 16 +- apps/server/src/models/post/post.service.ts | 1 + apps/server/src/upload/upload.module.ts | 18 +- .../src/app/main/letter/write/SendCard.tsx | 4 +- .../components/layout/admin/AdminHeader.tsx | 2 +- .../src/components/models/post/LetterCard.tsx | 90 +++++---- .../models/post/detail/PostCommentCard.tsx | 57 ++---- .../models/post/detail/PostCommentList.tsx | 15 +- .../models/post/detail/PostHeader.tsx | 171 ------------------ .../models/post/detail/PostHeader/Content.tsx | 35 ++++ .../models/post/detail/PostHeader/Header.tsx | 104 +++++++++++ .../post/detail/PostHeader/PostHeader.tsx | 20 ++ .../post/detail/PostHeader/TitleSection.tsx | 48 ----- .../models/post/detail/PostResources.tsx | 53 ++++++ .../post/detail/layout/PostDetailLayout.tsx | 3 +- config/nginx/conf.d/web.conf | 8 +- 18 files changed, 329 insertions(+), 337 deletions(-) delete mode 100644 apps/web/src/components/models/post/detail/PostHeader.tsx create mode 100644 apps/web/src/components/models/post/detail/PostHeader/Content.tsx create mode 100644 apps/web/src/components/models/post/detail/PostHeader/Header.tsx create mode 100644 apps/web/src/components/models/post/detail/PostHeader/PostHeader.tsx delete mode 100644 apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx create mode 100644 apps/web/src/components/models/post/detail/PostResources.tsx diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 3bc892f..94013f0 100755 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -23,12 +23,12 @@ import { UploadModule } from './upload/upload.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局可用 - envFilePath: '.env' + envFilePath: '.env', }), ScheduleModule.forRoot(), JwtModule.register({ global: true, - secret: env.JWT_SECRET + secret: env.JWT_SECRET, }), WebSocketModule, TrpcModule, @@ -42,11 +42,13 @@ import { UploadModule } from './upload/upload.module'; MinioModule, CollaborationModule, RealTimeModule, - UploadModule + UploadModule, + ], + providers: [ + { + provide: APP_FILTER, + useClass: ExceptionsFilter, + }, ], - providers: [{ - provide: APP_FILTER, - useClass: ExceptionsFilter, - }], }) -export class AppModule { } +export class AppModule {} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 1875a5f..8b82ed1 100755 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -8,7 +8,7 @@ async function bootstrap() { // 启用 CORS 并允许所有来源 app.enableCors({ - origin: "*", + origin: '*', }); const wsService = app.get(WebSocketService); await wsService.initialize(app.getHttpServer()); @@ -18,6 +18,5 @@ async function bootstrap() { const port = process.env.SERVER_PORT || 3000; await app.listen(port); - } bootstrap(); diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index e1d1b33..678452e 100755 --- a/apps/server/src/models/post/post.router.ts +++ b/apps/server/src/models/post/post.router.ts @@ -17,7 +17,7 @@ export class PostRouter { constructor( private readonly trpc: TrpcService, private readonly postService: PostService, - ) { } + ) {} router = this.trpc.router({ create: this.trpc.protectProcedure .input(PostCreateArgsSchema) @@ -97,12 +97,14 @@ export class PostRouter { return await this.postService.findManyWithCursor(input, staff, ip); }), findManyWithPagination: this.trpc.procedure - .input(z.object({ - page: z.number(), - pageSize: z.number().optional(), - where: PostWhereInputSchema.optional(), - select: PostSelectSchema.optional() - })) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .input( + z.object({ + page: z.number(), + pageSize: z.number().optional(), + where: PostWhereInputSchema.optional(), + select: PostSelectSchema.optional(), + }), + ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input }) => { return await this.postService.findManyWithPagination(input); }), diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 5372060..05637e2 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -33,6 +33,7 @@ export class PostService extends BaseService { args: Prisma.PostCreateArgs, params: { staff?: UserProfile; tx?: Prisma.PostDelegate }, ) { + console.log('params?.staff?.id', params?.staff?.id); args.data.authorId = params?.staff?.id; args.data.updatedAt = new Date(); // args.data.resources diff --git a/apps/server/src/upload/upload.module.ts b/apps/server/src/upload/upload.module.ts index 0a54ccb..6c8e1b0 100644 --- a/apps/server/src/upload/upload.module.ts +++ b/apps/server/src/upload/upload.module.ts @@ -5,13 +5,13 @@ import { TusService } from './tus.service'; import { ResourceModule } from '@server/models/resource/resource.module'; @Module({ - imports: [ - BullModule.registerQueue({ - name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 - }), - ResourceModule - ], - controllers: [UploadController], - providers: [TusService], + imports: [ + BullModule.registerQueue({ + name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 + }), + ResourceModule, + ], + controllers: [UploadController], + providers: [TusService], }) -export class UploadModule { } \ No newline at end of file +export class UploadModule {} diff --git a/apps/web/src/app/main/letter/write/SendCard.tsx b/apps/web/src/app/main/letter/write/SendCard.tsx index 3c67a97..1099264 100644 --- a/apps/web/src/app/main/letter/write/SendCard.tsx +++ b/apps/web/src/app/main/letter/write/SendCard.tsx @@ -29,7 +29,7 @@ export function SendCard({ staff, termId }: SendCardProps) { {staff.meta?.photoUrl ? ( {staff.showname} ) : ( @@ -61,7 +61,7 @@ export function SendCard({ staff, termId }: SendCardProps) {

- {staff.showname} + {staff?.showname}

diff --git a/apps/web/src/components/layout/admin/AdminHeader.tsx b/apps/web/src/components/layout/admin/AdminHeader.tsx index 6a2a22a..a62bf3d 100644 --- a/apps/web/src/components/layout/admin/AdminHeader.tsx +++ b/apps/web/src/components/layout/admin/AdminHeader.tsx @@ -63,7 +63,7 @@ const AdminHeader: React.FC = ({ const localState = { user: { id: user.id, - showname: user.showname || user.username, + showname: user?.showname || user.username, deptName: user.department?.name, sessionId, }, diff --git a/apps/web/src/components/models/post/LetterCard.tsx b/apps/web/src/components/models/post/LetterCard.tsx index 9647bfc..0c9e6a9 100644 --- a/apps/web/src/components/models/post/LetterCard.tsx +++ b/apps/web/src/components/models/post/LetterCard.tsx @@ -1,14 +1,22 @@ -import { EyeOutlined, LikeOutlined, LikeFilled, UserOutlined, BankOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons'; -import { Button, Typography, Space, Tooltip } from 'antd'; -import toast from 'react-hot-toast'; -import { useState } from 'react'; -import { getBadgeStyle } from '@web/src/app/main/letter/list/utils'; -import { PostDto } from '@nice/common'; -import dayjs from 'dayjs'; +import { + EyeOutlined, + LikeOutlined, + LikeFilled, + UserOutlined, + BankOutlined, + CalendarOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import { Button, Typography, Space, Tooltip } from "antd"; +import toast from "react-hot-toast"; +import { useState } from "react"; +import { getBadgeStyle } from "@web/src/app/main/letter/list/utils"; +import { PostDto } from "@nice/common"; +import dayjs from "dayjs"; const { Title, Paragraph, Text } = Typography; interface LetterCardProps { - letter: PostDto; + letter: PostDto; } export function LetterCard({ letter }: LetterCardProps) { @@ -45,32 +53,35 @@ export function LetterCard({ letter }: LetterCardProps) { className="text-primary transition-all duration-300 relative before:absolute before:bottom-0 before:left-0 before:w-0 before:h-[2px] before:bg-primary-600 group-hover:before:w-full before:transition-all before:duration-300 - group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md" - > - {letter.title} - - + group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md"> + {letter.title} + + +
- - - {/* Meta Info */} -
- - - - {letter.author.showname} - - | - - - {letter.author.department.name} - - - - - {dayjs(letter.createdAt).format('YYYY-MM-DD')} - -
+ {/* Meta Info */} +
+ + + + + {letter.author?.showname || + letter?.author?.username} + + + | + + + {letter.author?.department?.name} + + + + + + {dayjs(letter.createdAt).format("YYYY-MM-DD")} + + +
{/* Content Preview */} {letter.content && ( @@ -84,12 +95,12 @@ export function LetterCard({ letter }: LetterCardProps) { )} - {/* Badges & Interactions */} -
- - - - + {/* Badges & Interactions */} +
+ + + +
@@ -140,7 +151,6 @@ export function Badge({ transition-all duration-200 ease-in-out transform hover:scale-105 ${className} `}> - {value?.toUpperCase()} ) diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 828cbaf..e640773 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -9,6 +9,7 @@ import { PostDetailContext } from "./context/PostDetailContext"; import { LikeFilled, LikeOutlined } from "@ant-design/icons"; import PostLikeButton from "./PostHeader/PostLikeButton"; import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar"; +import PostResources from "./PostResources"; export default function PostCommentCard({ post, @@ -19,40 +20,6 @@ export default function PostCommentCard({ index: number; isReceiverComment: boolean; }) { - const { user } = useContext(PostDetailContext); - const { like, unLike } = useVisitor(); - const [liked, setLiked] = useState(post?.liked || false); - const [likeCount, setLikeCount] = useState(post?.likes || 0); - - async function likeThisPost() { - if (!liked) { - try { - setLikeCount((prev) => prev + 1); - setLiked(true); - like.mutateAsync({ - data: { - visitorId: user?.id || null, - postId: post.id, - type: VisitType.LIKE, - }, - }); - } catch (error) { - console.error("Failed to like post:", error); - setLikeCount((prev) => prev - 1); - setLiked(false); - } - } else { - setLikeCount((prev) => prev - 1); - setLiked(false); - unLike.mutateAsync({ - where: { - visitorId: user?.id || null, - postId: post.id, - type: VisitType.LIKE, - }, - }); - } - } return (
-
+
- + {post.author?.showname || "匿名用户"} - + {dayjs(post?.createdAt).format( "YYYY-MM-DD HH:mm" )} {isReceiverComment && ( - - 官方回答 - +
+ + 官方回答 + +
)}
{/* 添加有帮助按钮 */} - +
+
+ {`#${index + 1}`} + +
+
+
diff --git a/apps/web/src/components/models/post/detail/PostCommentList.tsx b/apps/web/src/components/models/post/detail/PostCommentList.tsx index b0b7767..2257f58 100644 --- a/apps/web/src/components/models/post/detail/PostCommentList.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentList.tsx @@ -33,7 +33,7 @@ export default function PostCommentList() { }, select: postDetailSelect, orderBy: [{ createdAt: "desc" }], - take: 3, + take: 5, }), [post, receiverIds] ); @@ -43,11 +43,14 @@ export default function PostCommentList() { where: { parentId: post?.id, type: PostType.POST_COMMENT, - authorId: { notIn: receiverIds }, + OR: [ + { authorId: null }, // 允许 authorId 为 null + { authorId: { notIn: receiverIds } }, // 排除 receiverIds 中的 authorId + ], }, select: postDetailSelect, orderBy: [{ createdAt: "desc" }], - take: 3, + take: 5, }), [post, receiverIds] ); @@ -147,6 +150,12 @@ export default function PostCommentList() { animate={{ opacity: 1, y: 0 }} className="text-center py-12 text-slate-500"> 暂无回复,来发表第一条回复吧 + ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader.tsx b/apps/web/src/components/models/post/detail/PostHeader.tsx deleted file mode 100644 index 63c6ba0..0000000 --- a/apps/web/src/components/models/post/detail/PostHeader.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useContext } from "react"; -import { PostDetailContext } from "./context/PostDetailContext"; -import { motion } from "framer-motion"; -import { - CalendarIcon, - UserCircleIcon, - LockClosedIcon, - LockOpenIcon, - StarIcon, - ClockIcon, - EyeIcon, - ChatBubbleLeftIcon, -} from "@heroicons/react/24/outline"; -import { Button, Typography, Space, Tooltip } from "antd"; -import { useVisitor } from "@nice/client"; -import { PostState, VisitType } from "@nice/common"; -import { - CalendarOutlined, - ClockCircleOutlined, - CommentOutlined, - EyeOutlined, - FileTextOutlined, - FolderOutlined, - LikeFilled, - LikeOutlined, - LockOutlined, - UnlockOutlined, - UserOutlined, -} from "@ant-design/icons"; -import dayjs from "dayjs"; -import { TitleSection } from "./PostHeader/TitleSection"; -import { - AuthorBadge, - DateBadge, - TermBadge, - UpdatedBadge, - VisibilityBadge, -} from "./PostHeader/InfoBadge"; -import { StatsSection } from "./PostHeader/StatsSection"; -import { PostBadge } from "./badge/PostBadge"; - -const { Title, Paragraph, Text } = Typography; -export default function PostHeader() { - const { post, user } = useContext(PostDetailContext); - const { like, unLike } = useVisitor(); - - function likeThisPost() { - if (!post?.liked) { - post.likes += 1; - post.liked = true; - like.mutateAsync({ - data: { - visitorId: user?.id || null, - postId: post.id, - type: VisitType.LIKE, - }, - }); - } else { - post.likes -= 1; - post.liked = false; - unLike.mutateAsync({ - where: { - visitorId: user?.id || null, - postId: post.id, - type: VisitType.LIKE, - }, - }); - } - } - - return ( - - {/* Corner Decorations */} -
-
- - {/* Title Section */} - - - -
- {/* 收件人信息行 */} - - - 收件人: - - {post?.receivers?.map((receiver, index) => ( - - {receiver?.showname} - - ))} - - - {/* First Row - Basic Info */} -
- {/* Author Info Badge */} - - - 发件人: - - {post?.author?.showname || "匿名用户"} - - - | - {/* Date Info Badge */} - - - - - 创建于: - {dayjs(post?.createdAt).format("YYYY-MM-DD")} - - - | - {/* Last Updated Badge */} - - - - 更新于: - {dayjs(post?.updatedAt).format("YYYY-MM-DD")} - - - | - {/* Visibility Status Badge */} - - {post?.isPublic ? ( - - ) : ( - - )} - {post?.isPublic ? "公开" : "私信"} - -
- {/* Second Row - Term and Tags */} - {post?.meta?.tags?.length > 0 && ( -
- {/* Tags Badges */} - {post.meta.tags.length > 0 && - post.meta.tags.map((tag, index) => ( - - - - ))} -
- )} -
- - {/* Content Section */} - -
- - - - {/* Stats Section */} - - - ); -} diff --git a/apps/web/src/components/models/post/detail/PostHeader/Content.tsx b/apps/web/src/components/models/post/detail/PostHeader/Content.tsx new file mode 100644 index 0000000..b9f3da5 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/Content.tsx @@ -0,0 +1,35 @@ +import { useContext } from "react"; +import { PostDetailContext } from "../context/PostDetailContext"; +import { motion } from "framer-motion"; + +import { StatsSection } from "./StatsSection"; + +import PostResources from "../PostResources"; +export default function Content() { + const { post, user } = useContext(PostDetailContext); + return ( + + +
+ + {/*
{post.resources?.map((resource) => {})}
*/} + + + {/* Stats Section */} + + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/Header.tsx b/apps/web/src/components/models/post/detail/PostHeader/Header.tsx new file mode 100644 index 0000000..41a839b --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/Header.tsx @@ -0,0 +1,104 @@ +import { useContext } from "react"; +import { PostDetailContext } from "../context/PostDetailContext"; +import { Space, Typography } from "antd"; +import { PostBadge } from "../badge/PostBadge"; +import { + CalendarOutlined, + ClockCircleOutlined, + LockOutlined, + UnlockOutlined, + UserOutlined, +} from "@ant-design/icons"; +import dayjs from "dayjs"; +const { Title, Paragraph, Text } = Typography; +export default function Header() { + const { post, user } = useContext(PostDetailContext); + return ( +
+
+ {/* 主标题 */} +
+

+ {post?.title} + + + +

+
+
+ {/* 收件人信息行 */} + + + 收件人: + + {post?.receivers?.map((receiver, index) => ( + + {receiver?.showname} + + ))} + + + {/* First Row - Basic Info */} +
+ {/* Author Info Badge */} + + + 发件人: + + {post?.author?.showname || "匿名用户"} + + + | + {/* Date Info Badge */} + + + + + 创建于: + {dayjs(post?.createdAt).format("YYYY-MM-DD")} + + + | + {/* Last Updated Badge */} + + + + 更新于: + {dayjs(post?.updatedAt).format("YYYY-MM-DD")} + + + | + {/* Visibility Status Badge */} + + {post?.isPublic ? ( + + ) : ( + + )} + + {post?.isPublic ? "公开" : "私信"} + + +
+ {/* Second Row - Term and Tags */} + {post?.meta?.tags?.length > 0 && ( +
+ {/* Tags Badges */} + {post.meta.tags.length > 0 && + post.meta.tags.map((tag, index) => ( + + + + ))} +
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostHeader.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostHeader.tsx new file mode 100644 index 0000000..da12fb9 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/PostHeader.tsx @@ -0,0 +1,20 @@ +import { useContext } from "react"; +import { PostDetailContext } from "../context/PostDetailContext"; +import { motion } from "framer-motion"; + +import { StatsSection } from "./StatsSection"; + +import PostResources from "../PostResources"; +import Header from "./Header"; +import Content from "./Content"; + +export default function PostHeader() { + const { post, user } = useContext(PostDetailContext); + + return ( + <> +
+ + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx deleted file mode 100644 index 4de55f6..0000000 --- a/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { motion } from "framer-motion"; -import { Space, Tag } from "antd"; -import { PostState } from "@nice/common"; -import { - ClockIcon, - CheckCircleIcon, - ExclamationCircleIcon, -} from "@heroicons/react/24/outline"; -import { Badge } from "@web/src/app/main/letter/list/LetterCard"; -import { useContext } from "react"; -import { PostDetailContext } from "../context/PostDetailContext"; -import { PostBadge } from "../badge/PostBadge"; - -interface TitleSectionProps { - title: string; - state: PostState; -} - -const stateLabels = { - [PostState.PENDING]: "待处理", - [PostState.PROCESSING]: "处理中", - [PostState.RESOLVED]: "已完成", -}; - -export function TitleSection() { - const { post, user } = useContext(PostDetailContext); - return ( - - {/* Decorative Line */} - {/*
*/} - - {/* Title */} -

- {post?.title} -

- - - - - - {/* */} - - ); -} diff --git a/apps/web/src/components/models/post/detail/PostResources.tsx b/apps/web/src/components/models/post/detail/PostResources.tsx new file mode 100644 index 0000000..77daa43 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostResources.tsx @@ -0,0 +1,53 @@ +import React, { useContext, useMemo } from "react"; +import { Image, Button } from "antd"; +import { DownloadOutlined } from "@ant-design/icons"; +import { PostDetailContext } from "./context/PostDetailContext"; +import { env } from "@web/src/env"; +import dayjs from "dayjs"; +import { PostDto } from "packages/common/dist"; + +export default function PostResources({ post }: { post: PostDto }) { + const { user } = useContext(PostDetailContext); + const resources = useMemo(() => { + return post?.resources?.map((resource) => ({ + url: `${env.SERVER_IP}/uploads/${resource.url}`, + title: resource.title, + })); + }, [post]); + + const isImage = (url: string) => { + return /\.(png|jpg|jpeg|gif|webp)$/i.test(url); + }; + + return ( +
+ {resources?.map((resource) => ( +
+ {isImage(resource.url) ? ( + <> + {resource.title} + + ) : ( + + )} +
+ ))} +
+ ); +} diff --git a/apps/web/src/components/models/post/detail/layout/PostDetailLayout.tsx b/apps/web/src/components/models/post/detail/layout/PostDetailLayout.tsx index 21fecd8..676dd62 100644 --- a/apps/web/src/components/models/post/detail/layout/PostDetailLayout.tsx +++ b/apps/web/src/components/models/post/detail/layout/PostDetailLayout.tsx @@ -1,7 +1,8 @@ import { motion } from "framer-motion"; import { useContext, useEffect } from "react"; import { PostDetailContext } from "../context/PostDetailContext"; -import PostHeader from "../PostHeader"; +import PostHeader from "../PostHeader/PostHeader"; +import WriteHeader from "../PostHeader/Header"; import PostCommentEditor from "../PostCommentEditor"; import PostCommentList from "../PostCommentList"; import { useVisitor } from "@nice/client"; diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index bf90259..361e802 100644 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -71,11 +71,11 @@ server { # 文件访问认证 # 通过内部认证服务验证 - auth_request /auth-file; + # auth_request /auth-file; # 存储认证状态和用户信息 - auth_request_set $auth_status $upstream_status; - auth_request_set $auth_user_id $upstream_http_x_user_id; - auth_request_set $auth_resource_type $upstream_http_x_resource_type; + # auth_request_set $auth_status $upstream_status; + # auth_request_set $auth_user_id $upstream_http_x_user_id; + # auth_request_set $auth_resource_type $upstream_http_x_resource_type; # 不缓存 expires 0; # 私有缓存,禁止转换