From 26cc1e064e232209ec98f0d26e0c9e1eebcc9b8d Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sat, 25 Jan 2025 02:28:28 +0800 Subject: [PATCH 1/2] add0125-0228 --- .../common/uploader/TusUploader.tsx | 199 +++++++++++++++++- .../models/post/detail/PostCommentEditor.tsx | 12 +- .../editor/context/LetterEditorContext.tsx | 13 +- .../post/editor/form/LetterBasicForm.tsx | 24 ++- apps/web/src/hooks/useTusUpload.ts | 18 +- packages/common/prisma/schema.prisma | 9 +- packages/common/src/select.ts | 12 +- 7 files changed, 263 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 4211322..b3ec1a3 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -1,7 +1,196 @@ -import React, { useEffect, useState } from "react"; -import { UploadOutlined } from "@ant-design/icons"; -import { Form, Upload, message } from "antd"; +import React, { useCallback, useState } from "react"; +import { + UploadOutlined, + CheckCircleOutlined, + DeleteOutlined, +} from "@ant-design/icons"; +import { Upload, message, Progress, Button } from "antd"; +import type { UploadFile } from "antd"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; -export const TusUploader = ({ value = [], onChange }) => { - return ; +export interface TusUploaderProps { + value?: string[]; + onChange?: (value: string[]) => void; +} + +interface UploadingFile { + name: string; + progress: number; + status: "uploading" | "done" | "error"; + fileId?: string; +} + +export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { + const { handleFileUpload } = useTusUpload(); + const [uploadingFiles, setUploadingFiles] = useState([]); + const [completedFiles, setCompletedFiles] = useState(() => + value?.map(fileId => ({ + name: `File ${fileId}`, // We could fetch the actual filename if needed + progress: 1, + status: 'done' as const, + fileId + })) || [] + ); + const [uploadResults, setUploadResults] = useState(value || []); + + const handleRemoveFile = useCallback( + (fileId: string) => { + setCompletedFiles((prev) => + prev.filter((f) => f.fileId !== fileId) + ); + const newResults = uploadResults.filter(id => id !== fileId); + setUploadResults(newResults); + onChange?.(newResults); + }, + [uploadResults, onChange] + ); + + const handleChange = useCallback( + async (fileList: UploadFile | UploadFile[]) => { + const files = Array.isArray(fileList) ? fileList : [fileList]; + console.log("files", files); + // 验证文件对象 + if (!files.every((f) => f instanceof File)) { + message.error("Invalid file format"); + return false; + } + + const newFiles: UploadingFile[] = files.map((f) => ({ + name: f.name, + progress: 0, + status: "uploading" as const, + })); + setUploadingFiles((prev) => [...prev, ...newFiles]); + const newUploadResults: string[] = []; + + try { + for (const [index, f] of files.entries()) { + if (!f) { + throw new Error(`File ${f.name} is invalid`); + } + + const fileId = await new Promise( + (resolve, reject) => { + handleFileUpload( + f as File, + (result) => { + console.log("Upload success:", result); + const completedFile = { + name: f.name, + progress: 1, + status: "done" as const, + fileId: result.fileId, + }; + setCompletedFiles((prev) => [ + ...prev, + completedFile, + ]); + setUploadingFiles((prev) => + prev.filter((_, i) => i !== index) + ); + resolve(result.fileId); + }, + (error) => { + console.error("Upload error:", error); + reject(error); + } + ); + } + ); + newUploadResults.push(fileId); + } + + // Update with all uploaded files + const newValue = Array.from(new Set([...uploadResults, ...newUploadResults])); + setUploadResults(newValue); + onChange?.(newValue); + message.success(`${files.length} files uploaded successfully`); + } catch (error) { + console.error("Upload error details:", error); + message.error( + `Upload failed: ${error instanceof Error ? error.message : "Unknown error"}` + ); + setUploadingFiles((prev) => + prev.map((f) => ({ ...f, status: "error" })) + ); + } + + return false; + }, + [uploadResults, onChange, handleFileUpload] + ); + + return ( +
+ +

+ +

+

+ Click or drag file to this area to upload +

+

+ Support for a single or bulk upload of files +

+
+ + {/* Uploading Files */} + {uploadingFiles.length > 0 && ( +
+
Uploading Files
+ {uploadingFiles.map((file, index) => ( +
+
+
{file.name}
+
+ +
+ ))} +
+ )} + + {/* Completed Files */} + {completedFiles.length > 0 && ( +
+
Uploaded Files
+ {completedFiles.map((file, index) => ( +
+
+ +
{file.name}
+
+
+ ))} +
+ )} +
+ ); }; diff --git a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx index 292a883..8575a74 100644 --- a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx @@ -14,6 +14,7 @@ export default function PostCommentEditor() { const { post } = useContext(PostDetailContext); const [content, setContent] = useState(""); const [isPreview, setIsPreview] = useState(false); + const [fileIds, setFileIds] = useState([]); const { create } = usePost(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -26,8 +27,14 @@ export default function PostCommentEditor() { await create.mutateAsync({ data: { type: PostType.POST_COMMENT, + parentId: post?.id, content: content, + resources: { + connect: fileIds.filter(Boolean).map((id) => ({ + fileId: id, + })), + }, }, }); toast.success("发布成功!"); @@ -83,7 +90,10 @@ export default function PostCommentEditor() { )} - + { + setFileIds(value); + }}>
({ + id, + })), + }, receivers: { connect: [receiverId].filter(Boolean).map((id) => ({ id, @@ -57,8 +62,10 @@ export function LetterFormProvider({ ...data, resources: data.resources?.length ? { - connect: data.resources.map((id) => ({ - id, + connect: ( + data.resources?.filter(Boolean) || [] + ).map((fileId) => ({ + fileId, })), } : undefined, diff --git a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index f5327b7..0edcefc 100644 --- a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -11,6 +11,7 @@ import { import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; import { PostBadge } from "../../detail/badge/PostBadge"; +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; export function LetterBasicForm() { const { onSubmit, receiverId, termId, form } = useLetterEditor(); @@ -132,8 +133,27 @@ export function LetterBasicForm() {
- - + {/* 内容输入框 */} +
+ + + 附件 + + } + name="resources" + labelCol={{ span: 24 }} + wrapperCol={{ span: 24 }}> +
+ + form.setFieldValue("resources", resources) + } + /> +
+
+
void, onError: (error: Error) => void ) => { + if (!file || !file.name || !file.type) { + const error = new Error('Invalid file provided'); + setUploadError(error.message); + onError(error); + return; + } + setIsUploading(true); setProgress(0); setUploadError(null); - const upload = new tus.Upload(file, { + + try { + const upload = new tus.Upload(file, { endpoint: "http://localhost:3000/upload", retryDelays: [0, 1000, 3000, 5000], metadata: { @@ -73,7 +82,14 @@ export function useTusUpload() { }, }); upload.start(); + } catch (error) { + const err = error instanceof Error ? error : new Error("Upload failed"); + setIsUploading(false); + setUploadError(err.message); + onError(err); + } }; + return { progress, isUploading, diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 4a3a901..fe37e9e 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -27,7 +27,8 @@ model Taxonomy { model Term { id String @id @default(cuid()) name String - posts Post[] + // posts Post[] + posts Post[] @relation("post_term") taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id]) taxonomyId String? @map("taxonomy_id") order Float? @map("order") @@ -193,8 +194,10 @@ model Post { content String? // 帖子内容,可为空 domainId String? @map("domain_id") - term Term? @relation(fields: [termId], references: [id]) - termId String? @map("term_id") + // term Term? @relation(fields: [termId], references: [id]) + // termId String? @map("term_id") + // 添加多对多关系 + terms Term[] @relation("post_term") // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @map("updated_at") diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 8208654..668bd06 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -12,16 +12,10 @@ export const postDetailSelect: Prisma.PostSelect = { resources: true, createdAt: true, updatedAt: true, - termId: true, - term: { + + terms: { include: { - taxonomy: { - select: { - id: true, - slug: true, - name: true, - }, - }, + }, }, authorId: true, From db2e3a044be98b3cbf96e02d3e9bdca173db9240 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sat, 25 Jan 2025 19:51:08 +0800 Subject: [PATCH 2/2] 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; # 私有缓存,禁止转换