This commit is contained in:
ditiqi 2025-01-25 19:51:08 +08:00
parent 455c82911d
commit db2e3a044b
18 changed files with 329 additions and 337 deletions

View File

@ -23,12 +23,12 @@ import { UploadModule } from './upload/upload.module';
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, // 全局可用 isGlobal: true, // 全局可用
envFilePath: '.env' envFilePath: '.env',
}), }),
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
JwtModule.register({ JwtModule.register({
global: true, global: true,
secret: env.JWT_SECRET secret: env.JWT_SECRET,
}), }),
WebSocketModule, WebSocketModule,
TrpcModule, TrpcModule,
@ -42,11 +42,13 @@ import { UploadModule } from './upload/upload.module';
MinioModule, MinioModule,
CollaborationModule, CollaborationModule,
RealTimeModule, RealTimeModule,
UploadModule UploadModule,
],
providers: [
{
provide: APP_FILTER,
useClass: ExceptionsFilter,
},
], ],
providers: [{
provide: APP_FILTER,
useClass: ExceptionsFilter,
}],
}) })
export class AppModule { } export class AppModule {}

View File

@ -8,7 +8,7 @@ async function bootstrap() {
// 启用 CORS 并允许所有来源 // 启用 CORS 并允许所有来源
app.enableCors({ app.enableCors({
origin: "*", origin: '*',
}); });
const wsService = app.get(WebSocketService); const wsService = app.get(WebSocketService);
await wsService.initialize(app.getHttpServer()); await wsService.initialize(app.getHttpServer());
@ -18,6 +18,5 @@ async function bootstrap() {
const port = process.env.SERVER_PORT || 3000; const port = process.env.SERVER_PORT || 3000;
await app.listen(port); await app.listen(port);
} }
bootstrap(); bootstrap();

View File

@ -17,7 +17,7 @@ export class PostRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly postService: PostService, private readonly postService: PostService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
create: this.trpc.protectProcedure create: this.trpc.protectProcedure
.input(PostCreateArgsSchema) .input(PostCreateArgsSchema)
@ -97,12 +97,14 @@ export class PostRouter {
return await this.postService.findManyWithCursor(input, staff, ip); return await this.postService.findManyWithCursor(input, staff, ip);
}), }),
findManyWithPagination: this.trpc.procedure findManyWithPagination: this.trpc.procedure
.input(z.object({ .input(
page: z.number(), z.object({
pageSize: z.number().optional(), page: z.number(),
where: PostWhereInputSchema.optional(), pageSize: z.number().optional(),
select: PostSelectSchema.optional() where: PostWhereInputSchema.optional(),
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword select: PostSelectSchema.optional(),
}),
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => { .query(async ({ input }) => {
return await this.postService.findManyWithPagination(input); return await this.postService.findManyWithPagination(input);
}), }),

View File

@ -33,6 +33,7 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
args: Prisma.PostCreateArgs, args: Prisma.PostCreateArgs,
params: { staff?: UserProfile; tx?: Prisma.PostDelegate }, params: { staff?: UserProfile; tx?: Prisma.PostDelegate },
) { ) {
console.log('params?.staff?.id', params?.staff?.id);
args.data.authorId = params?.staff?.id; args.data.authorId = params?.staff?.id;
args.data.updatedAt = new Date(); args.data.updatedAt = new Date();
// args.data.resources // args.data.resources

View File

@ -5,13 +5,13 @@ import { TusService } from './tus.service';
import { ResourceModule } from '@server/models/resource/resource.module'; import { ResourceModule } from '@server/models/resource/resource.module';
@Module({ @Module({
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致
}), }),
ResourceModule ResourceModule,
], ],
controllers: [UploadController], controllers: [UploadController],
providers: [TusService], providers: [TusService],
}) })
export class UploadModule { } export class UploadModule {}

View File

@ -29,7 +29,7 @@ export function SendCard({ staff, termId }: SendCardProps) {
{staff.meta?.photoUrl ? ( {staff.meta?.photoUrl ? (
<img <img
src={staff.meta.photoUrl} src={staff.meta.photoUrl}
alt={staff.showname} alt={staff?.showname}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300" className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/> />
) : ( ) : (
@ -61,7 +61,7 @@ export function SendCard({ staff, termId }: SendCardProps) {
<div> <div>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-2xl font-semibold text-gray-900"> <h3 className="text-2xl font-semibold text-gray-900">
{staff.showname} {staff?.showname}
</h3> </h3>
<Badge status="success" /> <Badge status="success" />
</div> </div>

View File

@ -63,7 +63,7 @@ const AdminHeader: React.FC<AdminHeaderProps> = ({
const localState = { const localState = {
user: { user: {
id: user.id, id: user.id,
showname: user.showname || user.username, showname: user?.showname || user.username,
deptName: user.department?.name, deptName: user.department?.name,
sessionId, sessionId,
}, },

View File

@ -1,14 +1,22 @@
import { EyeOutlined, LikeOutlined, LikeFilled, UserOutlined, BankOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons'; import {
import { Button, Typography, Space, Tooltip } from 'antd'; EyeOutlined,
import toast from 'react-hot-toast'; LikeOutlined,
import { useState } from 'react'; LikeFilled,
import { getBadgeStyle } from '@web/src/app/main/letter/list/utils'; UserOutlined,
import { PostDto } from '@nice/common'; BankOutlined,
import dayjs from 'dayjs'; 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; const { Title, Paragraph, Text } = Typography;
interface LetterCardProps { interface LetterCardProps {
letter: PostDto; letter: PostDto;
} }
export function LetterCard({ letter }: LetterCardProps) { export function LetterCard({ letter }: LetterCardProps) {
@ -45,32 +53,35 @@ export function LetterCard({ letter }: LetterCardProps) {
className="text-primary transition-all duration-300 relative 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 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:before:w-full before:transition-all before:duration-300
group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md" group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md">
> {letter.title}
{letter.title} </a>
</a> </Title>
</Title> </div>
</div> {/* Meta Info */}
<div className="flex justify-between items-center text-sm text-secondary">
{/* Meta Info */} <Space size="middle">
<div className="flex justify-between items-center text-sm text-secondary"> <Space>
<Space size="middle"> <UserOutlined className="text-secondary-400" />
<Space> <Text strong>
<UserOutlined className="text-secondary-400" /> {letter.author?.showname ||
<Text strong>{letter.author.showname}</Text> letter?.author?.username}
</Space> </Text>
<Text type="secondary">|</Text> </Space>
<Space> <Text type="secondary">|</Text>
<BankOutlined className="text-secondary-400" /> <Space>
<Text>{letter.author.department.name}</Text> <BankOutlined className="text-secondary-400" />
</Space> <Text>{letter.author?.department?.name}</Text>
</Space> </Space>
<Space> </Space>
<CalendarOutlined className="text-secondary-400" /> <Space>
<Text type="secondary">{dayjs(letter.createdAt).format('YYYY-MM-DD')}</Text> <CalendarOutlined className="text-secondary-400" />
</Space> <Text type="secondary">
</div> {dayjs(letter.createdAt).format("YYYY-MM-DD")}
</Text>
</Space>
</div>
{/* Content Preview */} {/* Content Preview */}
{letter.content && ( {letter.content && (
@ -84,12 +95,12 @@ export function LetterCard({ letter }: LetterCardProps) {
</div> </div>
)} )}
{/* Badges & Interactions */} {/* Badges & Interactions */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Space size="small" wrap className="flex-1"> <Space size="small" wrap className="flex-1">
<Badge type="category" value={'11'} /> <Badge type="category" value={"11"} />
<Badge type="status" value={'22'} /> <Badge type="status" value={"22"} />
</Space> </Space>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-gray-500">
@ -140,7 +151,6 @@ export function Badge({
transition-all duration-200 ease-in-out transform hover:scale-105 transition-all duration-200 ease-in-out transform hover:scale-105
${className} ${className}
`}> `}>
{value?.toUpperCase()} {value?.toUpperCase()}
</span> </span>
) )

View File

@ -9,6 +9,7 @@ import { PostDetailContext } from "./context/PostDetailContext";
import { LikeFilled, LikeOutlined } from "@ant-design/icons"; import { LikeFilled, LikeOutlined } from "@ant-design/icons";
import PostLikeButton from "./PostHeader/PostLikeButton"; import PostLikeButton from "./PostHeader/PostLikeButton";
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar"; import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
import PostResources from "./PostResources";
export default function PostCommentCard({ export default function PostCommentCard({
post, post,
@ -19,40 +20,6 @@ export default function PostCommentCard({
index: number; index: number;
isReceiverComment: boolean; 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 ( return (
<motion.div <motion.div
className="bg-white rounded-lg shadow-sm border border-slate-200 p-4" className="bg-white rounded-lg shadow-sm border border-slate-200 p-4"
@ -67,24 +34,31 @@ export default function PostCommentCard({
}></CustomAvatar> }></CustomAvatar>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex flex-1 justify-between"> <div className="flex flex-1 justify-between ">
<div className="flex space-x-2" style={{ height: 40 }}> <div className="flex space-x-2" style={{ height: 40 }}>
<span className="font-medium text-slate-900"> <span className="flex font-medium text-slate-900">
{post.author?.showname || "匿名用户"} {post.author?.showname || "匿名用户"}
</span> </span>
<span className="text-sm text-slate-500"> <span className="flex text-sm text-slate-500">
{dayjs(post?.createdAt).format( {dayjs(post?.createdAt).format(
"YYYY-MM-DD HH:mm" "YYYY-MM-DD HH:mm"
)} )}
</span> </span>
{isReceiverComment && ( {isReceiverComment && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800"> <div className=" ">
<span className=" px-2 text-sm rounded-full bg-blue-100 text-blue-800">
</span>
</span>
</div>
)} )}
</div> </div>
{/* 添加有帮助按钮 */} {/* 添加有帮助按钮 */}
<PostLikeButton post={post}></PostLikeButton> <div>
<div className="flex justify-center items-center gap-2">
<span className=" text-sm text-slate-500">{`#${index + 1}`}</span>
<PostLikeButton post={post}></PostLikeButton>
</div>
</div>
</div> </div>
<div <div
@ -94,6 +68,7 @@ export default function PostCommentCard({
}} }}
dangerouslySetInnerHTML={{ __html: post.content || "" }} dangerouslySetInnerHTML={{ __html: post.content || "" }}
/> />
<PostResources post={post}></PostResources>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -33,7 +33,7 @@ export default function PostCommentList() {
}, },
select: postDetailSelect, select: postDetailSelect,
orderBy: [{ createdAt: "desc" }], orderBy: [{ createdAt: "desc" }],
take: 3, take: 5,
}), }),
[post, receiverIds] [post, receiverIds]
); );
@ -43,11 +43,14 @@ export default function PostCommentList() {
where: { where: {
parentId: post?.id, parentId: post?.id,
type: PostType.POST_COMMENT, type: PostType.POST_COMMENT,
authorId: { notIn: receiverIds }, OR: [
{ authorId: null }, // 允许 authorId 为 null
{ authorId: { notIn: receiverIds } }, // 排除 receiverIds 中的 authorId
],
}, },
select: postDetailSelect, select: postDetailSelect,
orderBy: [{ createdAt: "desc" }], orderBy: [{ createdAt: "desc" }],
take: 3, take: 5,
}), }),
[post, receiverIds] [post, receiverIds]
); );
@ -147,6 +150,12 @@ export default function PostCommentList() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-center py-12 text-slate-500"> className="text-center py-12 text-slate-500">
<Button
onClick={() => {
console.log(receiverIds);
}}>
123
</Button>
</motion.div> </motion.div>
); );
} }

View File

@ -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 (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative bg-gradient-to-br from-primary-250 via-primary-150 to--primary-350 rounded-lg p-6 shadow-lg border border-[#97A9C4]/30">
{/* Corner Decorations */}
<div className="absolute top-0 left-0 w-5 h-5 border-t-4 border-l-4 border-primary rounded-tl-lg" />
<div className="absolute bottom-0 right-0 w-5 h-5 border-b-4 border-r-4 border-primary rounded-br-lg" />
{/* Title Section */}
<TitleSection></TitleSection>
<div className="space-y-4">
{/* 收件人信息行 */}
<Space>
<UserOutlined className="text-secondary-400" />
<span className="text-secondary-400"></span>
{post?.receivers?.map((receiver, index) => (
<Text strong key={`${index}`}>
{receiver?.showname}
</Text>
))}
</Space>
{/* First Row - Basic Info */}
<div className="flex flex-wrap items-center gap-1">
{/* Author Info Badge */}
<Space>
<UserOutlined className="text-secondary-400" />
<span className="text-secondary-400"></span>
<Text strong>
{post?.author?.showname || "匿名用户"}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Date Info Badge */}
<Space>
<CalendarOutlined className="text-secondary-400" />
<Text>
:
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Last Updated Badge */}
<Space>
<ClockCircleOutlined className="text-secondary-400" />
<Text>
:
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Visibility Status Badge */}
<Space>
{post?.isPublic ? (
<UnlockOutlined className="text-secondary-400" />
) : (
<LockOutlined className="text-secondary-400" />
)}
<Text>{post?.isPublic ? "公开" : "私信"}</Text>
</Space>
</div>
{/* Second Row - Term and Tags */}
{post?.meta?.tags?.length > 0 && (
<div className="flex flex-wrap gap-1">
{/* Tags Badges */}
{post.meta.tags.length > 0 &&
post.meta.tags.map((tag, index) => (
<Space key={index}>
<PostBadge
type="tag"
value={`#${tag}`}></PostBadge>
</Space>
))}
</div>
)}
</div>
{/* Content Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mt-6 text-secondary-700">
<div
className="ql-editor p-0 space-y-4 leading-relaxed duration-300"
dangerouslySetInnerHTML={{ __html: post?.content || "" }}
/>
</motion.div>
{/* Stats Section */}
<StatsSection></StatsSection>
</motion.div>
);
}

View File

@ -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 (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative bg-white rounded-b-xl p-6 pt-2 shadow-lg border border-[#97A9C4]/30">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className=" text-secondary-700">
<div
className="ql-editor p-0 space-y-1 leading-relaxed duration-300"
dangerouslySetInnerHTML={{
__html: post?.content || "",
}}
/>
<PostResources post={post}></PostResources>
{/* <div>{post.resources?.map((resource) => {})}</div> */}
</motion.div>
{/* Stats Section */}
<StatsSection></StatsSection>
</motion.div>
);
}

View File

@ -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 (
<header className=" rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6">
<div className="flex flex-col space-y-6">
{/* 主标题 */}
<div>
<h1 className="text-3xl font-bold tracking-wider flex items-center gap-2">
{post?.title}
<PostBadge type="category" value={post?.term?.name} />
<PostBadge type="state" value={post?.state} />
</h1>
</div>
<div className="space-y-4">
{/* 收件人信息行 */}
<Space>
<UserOutlined className="text-white" />
<span className="text-white"></span>
{post?.receivers?.map((receiver, index) => (
<Text
strong
className="text-white"
key={`${index}`}>
{receiver?.showname}
</Text>
))}
</Space>
{/* First Row - Basic Info */}
<div className="flex flex-wrap items-center gap-1">
{/* Author Info Badge */}
<Space>
<UserOutlined className="text-white" />
<span className="text-white"></span>
<Text className="text-white" strong>
{post?.author?.showname || "匿名用户"}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Date Info Badge */}
<Space>
<CalendarOutlined className="text-white" />
<Text className="text-white">
:
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Last Updated Badge */}
<Space>
<ClockCircleOutlined className="text-white" />
<Text className="text-white">
:
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
</Text>
</Space>
<Text type="secondary">|</Text>
{/* Visibility Status Badge */}
<Space>
{post?.isPublic ? (
<UnlockOutlined className="text-white" />
) : (
<LockOutlined className="text-white" />
)}
<Text className="text-white">
{post?.isPublic ? "公开" : "私信"}
</Text>
</Space>
</div>
{/* Second Row - Term and Tags */}
{post?.meta?.tags?.length > 0 && (
<div className="flex flex-wrap gap-1">
{/* Tags Badges */}
{post.meta.tags.length > 0 &&
post.meta.tags.map((tag, index) => (
<Space key={index}>
<PostBadge
type="tag"
value={`#${tag}`}></PostBadge>
</Space>
))}
</div>
)}
</div>
</div>
</header>
);
}

View File

@ -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 (
<>
<Header></Header>
<Content></Content>
</>
);
}

View File

@ -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 (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="relative mb-6 flex items-center gap-4">
{/* Decorative Line */}
{/* <div className="absolute -left-2 top-1/2 -translate-y-1/2 w-1 h-8 bg-primary" /> */}
{/* Title */}
<h1 className="text-xl font-bold text-primary tracking-wider uppercase">
{post?.title}
</h1>
<Space size="small" wrap className="flex-1">
<PostBadge type="category" value={post?.term?.name} />
<PostBadge type="state" value={post?.state} />
</Space>
{/* </motion.div> */}
</motion.div>
);
}

View File

@ -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 (
<div className="flex flex-col gap-4">
{resources?.map((resource) => (
<div
key={resource.url}
className="flex items-center gap-4 mt-2 rounded-lg">
{isImage(resource.url) ? (
<>
<Image
src={resource.url}
alt={resource.title}
className="rounded-lg"
width={"100%"}
height={"auto"}
style={{ objectFit: "cover" }}
/>
</>
) : (
<Button
type="primary"
icon={<DownloadOutlined />}
href={resource.url}
download
className="bg-blue-600 hover:bg-blue-700">
{resource.title || "下载"}
</Button>
)}
</div>
))}
</div>
);
}

View File

@ -1,7 +1,8 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { PostDetailContext } from "../context/PostDetailContext"; import { PostDetailContext } from "../context/PostDetailContext";
import PostHeader from "../PostHeader"; import PostHeader from "../PostHeader/PostHeader";
import WriteHeader from "../PostHeader/Header";
import PostCommentEditor from "../PostCommentEditor"; import PostCommentEditor from "../PostCommentEditor";
import PostCommentList from "../PostCommentList"; import PostCommentList from "../PostCommentList";
import { useVisitor } from "@nice/client"; import { useVisitor } from "@nice/client";

View File

@ -71,11 +71,11 @@ server {
# 文件访问认证 # 文件访问认证
# 通过内部认证服务验证 # 通过内部认证服务验证
auth_request /auth-file; # auth_request /auth-file;
# 存储认证状态和用户信息 # 存储认证状态和用户信息
auth_request_set $auth_status $upstream_status; # auth_request_set $auth_status $upstream_status;
auth_request_set $auth_user_id $upstream_http_x_user_id; # 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_resource_type $upstream_http_x_resource_type;
# 不缓存 # 不缓存
expires 0; expires 0;
# 私有缓存,禁止转换 # 私有缓存,禁止转换