add
This commit is contained in:
parent
455c82911d
commit
db2e3a044b
|
@ -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 {}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
@ -33,6 +33,7 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
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
|
||||
|
|
|
@ -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 { }
|
||||
export class UploadModule {}
|
||||
|
|
|
@ -29,7 +29,7 @@ export function SendCard({ staff, termId }: SendCardProps) {
|
|||
{staff.meta?.photoUrl ? (
|
||||
<img
|
||||
src={staff.meta.photoUrl}
|
||||
alt={staff.showname}
|
||||
alt={staff?.showname}
|
||||
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 className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-2xl font-semibold text-gray-900">
|
||||
{staff.showname}
|
||||
{staff?.showname}
|
||||
</h3>
|
||||
<Badge status="success" />
|
||||
</div>
|
||||
|
|
|
@ -63,7 +63,7 @@ const AdminHeader: React.FC<AdminHeaderProps> = ({
|
|||
const localState = {
|
||||
user: {
|
||||
id: user.id,
|
||||
showname: user.showname || user.username,
|
||||
showname: user?.showname || user.username,
|
||||
deptName: user.department?.name,
|
||||
sessionId,
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
</a>
|
||||
</Title>
|
||||
group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md">
|
||||
{letter.title}
|
||||
</a>
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-secondary">
|
||||
<Space size="middle">
|
||||
<Space>
|
||||
<UserOutlined className="text-secondary-400" />
|
||||
<Text strong>{letter.author.showname}</Text>
|
||||
</Space>
|
||||
<Text type="secondary">|</Text>
|
||||
<Space>
|
||||
<BankOutlined className="text-secondary-400" />
|
||||
<Text>{letter.author.department.name}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
<Space>
|
||||
<CalendarOutlined className="text-secondary-400" />
|
||||
<Text type="secondary">{dayjs(letter.createdAt).format('YYYY-MM-DD')}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-secondary">
|
||||
<Space size="middle">
|
||||
<Space>
|
||||
<UserOutlined className="text-secondary-400" />
|
||||
<Text strong>
|
||||
{letter.author?.showname ||
|
||||
letter?.author?.username}
|
||||
</Text>
|
||||
</Space>
|
||||
<Text type="secondary">|</Text>
|
||||
<Space>
|
||||
<BankOutlined className="text-secondary-400" />
|
||||
<Text>{letter.author?.department?.name}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
<Space>
|
||||
<CalendarOutlined className="text-secondary-400" />
|
||||
<Text type="secondary">
|
||||
{dayjs(letter.createdAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Content Preview */}
|
||||
{letter.content && (
|
||||
|
@ -84,12 +95,12 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Space size="small" wrap className="flex-1">
|
||||
<Badge type="category" value={'11'} />
|
||||
<Badge type="status" value={'22'} />
|
||||
</Space>
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Space size="small" wrap className="flex-1">
|
||||
<Badge type="category" value={"11"} />
|
||||
<Badge type="status" value={"22"} />
|
||||
</Space>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<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
|
||||
${className}
|
||||
`}>
|
||||
|
||||
{value?.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
|
|
|
@ -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 (
|
||||
<motion.div
|
||||
className="bg-white rounded-lg shadow-sm border border-slate-200 p-4"
|
||||
|
@ -67,24 +34,31 @@ export default function PostCommentCard({
|
|||
}></CustomAvatar>
|
||||
</div>
|
||||
<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 }}>
|
||||
<span className="font-medium text-slate-900">
|
||||
<span className="flex font-medium text-slate-900">
|
||||
{post.author?.showname || "匿名用户"}
|
||||
</span>
|
||||
<span className="text-sm text-slate-500">
|
||||
<span className="flex text-sm text-slate-500">
|
||||
{dayjs(post?.createdAt).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
)}
|
||||
</span>
|
||||
{isReceiverComment && (
|
||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
||||
官方回答
|
||||
</span>
|
||||
<div className=" ">
|
||||
<span className=" px-2 text-sm rounded-full bg-blue-100 text-blue-800">
|
||||
官方回答
|
||||
</span>
|
||||
</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
|
||||
|
@ -94,6 +68,7 @@ export default function PostCommentCard({
|
|||
}}
|
||||
dangerouslySetInnerHTML={{ __html: post.content || "" }}
|
||||
/>
|
||||
<PostResources post={post}></PostResources>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
@ -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">
|
||||
暂无回复,来发表第一条回复吧
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log(receiverIds);
|
||||
}}>
|
||||
123
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
# 私有缓存,禁止转换
|
||||
|
|
Loading…
Reference in New Issue