diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index 678452e..0638e80 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) @@ -105,8 +105,10 @@ export class PostRouter { select: PostSelectSchema.optional(), }), ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.postService.findManyWithPagination(input); + .query(async ({ input, ctx }) => { + const { staff, req } = ctx; + const ip = getClientIp(req); + return await this.postService.findManyWithPagination(input, staff, ip); }), }); } diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 05637e2..10d786d 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -14,6 +14,7 @@ import { BaseService } from '../base/base.service'; import { DepartmentService } from '../department/department.service'; import { setPostRelation, updatePostState } from './utils'; import EventBus, { CrudOperation } from '@server/utils/event-bus'; +import { DefaultArgs } from '@prisma/client/runtime/library'; @Injectable() export class PostService extends BaseService { @@ -96,7 +97,24 @@ export class PostService extends BaseService { return { ...result, items }; }); } - + async findManyWithPagination( + args: { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; select?: Prisma.PostSelect; }, + staff?: UserProfile, + clientIp?: string, + ) { + if (!args.where) args.where = {}; + args.where.OR = await this.preFilter(args.where.OR, staff); + return this.wrapResult(super.findManyWithPagination(args), async (result) => { + const { items } = result; + await Promise.all( + items.map(async (item) => { + await setPostRelation({ data: item, staff, clientIp }); + await this.setPerms(item, staff); + }), + ); + return { ...result, items }; + }); + } protected async setPerms(data: Post, staff?: UserProfile) { if (!staff) return; const perms: ResPerm = { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 64cf549..90906fd 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -61,9 +61,7 @@ export async function setPostRelation(params: { readed, readedCount, liked, - // limitedComments, commentsCount, - // trouble }); // console.log('data', data); return data; // 明确返回修改后的数据 diff --git a/apps/web/src/app/main/letter/list/utils.ts b/apps/web/src/app/main/letter/list/utils.ts deleted file mode 100644 index 55548e1..0000000 --- a/apps/web/src/app/main/letter/list/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PostState } from "@nice/common"; - -export const BADGE_STYLES = { - priority: { - high: "bg-red-100 text-red-800", - medium: "bg-yellow-100 text-yellow-800", - low: "bg-green-100 text-green-800", - }, - category: { - complaint: "bg-orange-100 text-orange-800", - suggestion: "bg-blue-100 text-blue-800", - request: "bg-purple-100 text-purple-800", - feedback: "bg-teal-100 text-teal-800", - }, - status: { - [PostState.PENDING]: "bg-yellow-100 text-yellow-800", - [PostState.PROCESSING]: "bg-blue-100 text-blue-800", - [PostState.RESOLVED]: "bg-green-100 text-green-800", - }, -} as const; - -export const getBadgeStyle = ( - type: keyof typeof BADGE_STYLES, - value: string -): string => { - return ( - BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] || - "bg-gray-100 text-gray-800" - ); -}; diff --git a/apps/web/src/app/main/letter/progress/page.tsx b/apps/web/src/app/main/letter/progress/page.tsx index 7fb0805..eef95c6 100644 --- a/apps/web/src/app/main/letter/progress/page.tsx +++ b/apps/web/src/app/main/letter/progress/page.tsx @@ -2,64 +2,21 @@ import { useState } from "react"; import { Input, Button, Card, Steps, Tag, Spin, message } from "antd"; import { SearchOutlined, SafetyCertificateOutlined } from "@ant-design/icons"; import ProgressHeader from "./ProgressHeader"; +import { api } from "@nice/client"; +import { LetterBadge } from "@web/src/components/models/post/LetterBadge"; +import dayjs from "dayjs"; -interface FeedbackStatus { - status: "pending" | "processing" | "resolved"; - ticketId: string; - submittedDate: string; - lastUpdate: string; - title: string; -} const { Step } = Steps; export default function LetterProgressPage() { - const [feedbackId, setFeedbackId] = useState(""); - const [status, setStatus] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - - const validateInput = () => { - if (!feedbackId.trim()) { - setError("请输入有效的问题编号"); - return false; + const [letterId, setLetterId] = useState(); + const { data } = api.post.findFirst.useQuery({ + where: { + id: letterId } - if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) { - setError("问题编号格式不正确,应为USAF-YYYY-NNNN"); - return false; - } - setError(""); - return true; - }; + }, { enabled: Boolean(letterId) }) - const mockLookup = () => { - if (!validateInput()) return; - - setLoading(true); - setTimeout(() => { - setStatus({ - status: "processing", - ticketId: feedbackId, - submittedDate: "2025-01-15", - lastUpdate: "2025-01-21", - title: "Aircraft Maintenance Schedule Inquiry", - }); - setLoading(false); - }, 1000); - }; - - const getStatusColor = (status: string) => { - switch (status) { - case "pending": - return "orange"; - case "processing": - return "blue"; - case "resolved": - return "green"; - default: - return "gray"; - } - }; return (
} size="large" - value={feedbackId} - onChange={(e) => setFeedbackId(e.target.value)} + value={letterId} + onChange={(e) => setLetterId(e.target.value)} placeholder="请输入信件编码查询处理状态" - status={error ? "error" : ""} className="border border-gray-300" /> -
- {error && ( -

{error}

- )} + {/* Results Section */} - {status && ( + {data && (
{/* Header */}

- Ticket Details + 处理进度

- - {status.status} - +
{/* Details Grid */} @@ -118,31 +60,31 @@ export default function LetterProgressPage() { Ticket ID

- {status.ticketId} + {data.id}

- Submitted Date + 提交日期

- {status.submittedDate} + {dayjs(data.createdAt).format('YYYY-MM-DD HH:mm:ss')}

- Last Update + 最后更新

- {status.lastUpdate} + {dayjs(data.updatedAt).format('YYYY-MM-DD HH:mm:ss')}

- Subject + 标题

- {status.title} + {data.title}

@@ -151,9 +93,9 @@ export default function LetterProgressPage() {
, + hover: "hover:from-orange-100 hover:to-orange-200", + }, + suggestion: { + bg: "bg-gradient-to-r from-blue-50 to-blue-100", + text: "text-blue-800", + border: "border-blue-200", + icon: , + hover: "hover:from-blue-100 hover:to-blue-200", + }, + request: { + bg: "bg-gradient-to-r from-purple-50 to-purple-100", + text: "text-purple-800", + border: "border-purple-200", + icon: , + hover: "hover:from-purple-100 hover:to-purple-200", + }, + feedback: { + bg: "bg-gradient-to-r from-teal-50 to-teal-100", + text: "text-teal-800", + border: "border-teal-200", + icon: , + hover: "hover:from-teal-100 hover:to-teal-200", + }, + }, + state: { + [PostState.PENDING]: { + bg: "bg-gradient-to-r from-yellow-50 to-yellow-100", + text: "text-yellow-800", + border: "border-yellow-200", + icon: , + hover: "hover:from-yellow-100 hover:to-yellow-200", + }, + [PostState.PROCESSING]: { + bg: "bg-gradient-to-r from-blue-50 to-blue-100", + text: "text-blue-800", + border: "border-blue-200", + icon: , + hover: "hover:from-blue-100 hover:to-blue-200", + }, + [PostState.RESOLVED]: { + bg: "bg-gradient-to-r from-green-50 to-green-100", + text: "text-green-800", + border: "border-green-200", + icon: , + hover: "hover:from-green-100 hover:to-green-200", + }, + }, + tag: { + default: { + bg: "bg-gradient-to-r from-gray-50 to-gray-100", + text: "text-gray-800", + border: "border-gray-200", + icon: , + hover: "hover:from-gray-100 hover:to-gray-200", + } + }, +} as const; + +export const getBadgeStyle = ( + type: keyof typeof BADGE_STYLES, + value: string +) => { + const style = type === 'tag' + ? BADGE_STYLES.tag.default + : BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] || { + bg: "bg-gradient-to-r from-gray-50 to-gray-100", + text: "text-gray-800", + border: "border-gray-200", + icon: , + hover: "hover:from-gray-100 hover:to-gray-200", + }; + + return style; +}; + +export function LetterBadge({ + type, + value, + className = "", +}: { + type: "category" | "state" | "tag"; + value: string; + className?: string; +}) { + if (!value) return null; + + const style = getBadgeStyle(type, value); + + return ( + + {style.icon} + + {type === 'state' ? PostStateLabels[value] : value} + + + ); +} diff --git a/apps/web/src/components/models/post/LetterCard.tsx b/apps/web/src/components/models/post/LetterCard.tsx index 048d307..de28fad 100644 --- a/apps/web/src/components/models/post/LetterCard.tsx +++ b/apps/web/src/components/models/post/LetterCard.tsx @@ -1,158 +1,127 @@ import { - EyeOutlined, - LikeOutlined, - LikeFilled, - UserOutlined, - BankOutlined, - CalendarOutlined, - FileTextOutlined, + EyeOutlined, + LikeOutlined, + LikeFilled, + UserOutlined, + BankOutlined, + CalendarOutlined, + FileTextOutlined, + SendOutlined, } 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 { PostDto, PostStateLabels } from "@nice/common"; import dayjs from "dayjs"; +import PostLikeButton from "./detail/PostHeader/PostLikeButton"; +import { LetterBadge } from "./LetterBadge"; const { Title, Paragraph, Text } = Typography; interface LetterCardProps { - letter: PostDto; + letter: PostDto; } export function LetterCard({ letter }: LetterCardProps) { - const [likes, setLikes] = useState(0); - const [liked, setLiked] = useState(false); - const [views] = useState(Math.floor(Math.random() * 100)); // 模拟浏览量数据 + return ( +
{ + window.open(`/${letter.id}/detail`) + }} + className=" cursor-pointer w-full p-6 bg-white rounded-xl group relative overflow-hidden duration-300 hover:-translate-y-1 transition-all ease-in-out " + > +
+
+ + <a + href={`/${letter.id}/detail`} + target="_blank" - const handleLike = () => { - if (!liked) { - setLikes((prev) => prev + 1); - setLiked(true); - toast.success("已点赞!", { - icon: <LikeFilled className="text-blue-500" />, - className: "custom-message", - }); - } else { - setLikes((prev) => prev - 1); - setLiked(false); - toast("已取消点赞", { - className: "custom-message", - }); - } - }; + className="text-primary hover:text-primary-600 transition-colors duration-200 hover:underline"> + {letter.title} + </a> + +
+ {/* Meta Info */} +
+
+
+ + + {letter.author?.showname || '匿名用户'} + +
- return ( -
-
- {/* Title & Priority */} -
- - <a - href={`/${letter.id}/detail`} - target="_blank" - 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> - -
+ {letter.receivers.some(item => item.department?.name) && ( +
+ + item.department?.name).filter(Boolean).join(', ')}> + + {letter.receivers + .map(item => item.department?.name) + .filter(Boolean) + .slice(0, 2) + .join('、')} + {letter.receivers.filter(item => item.department?.name).length > 2 && ' 等'} + + +
+ )} - {/* Meta Info */} -
- - - - - {letter.author?.showname || - letter?.author?.username} - - - | - - - {letter.author?.department?.name} - - - - - - {dayjs(letter.createdAt).format("YYYY-MM-DD")} - - -
+ {letter.receivers.some(item => item.showname) && ( +
+ + item.showname).filter(Boolean).join(', ')}> + + {letter.receivers + .map(item => item.showname) + .filter(Boolean) + .slice(0, 2) + .join('、')} + {letter.receivers.filter(item => item.showname).length > 2 && ' 等'} + + +
+ )} +
+
+ + + {dayjs(letter.createdAt).format("YYYY-MM-DD")} + +
+
- {/* Content Preview */} - {letter.content && ( -
- - - {letter.content} - -
- )} + {/* Content Preview */} + {letter.content && ( + + {letter.content} + + )} +
+ + {letter.meta.tags.map(tag => ( + + ))} +
+ {/* Badges & Interactions */} +
+
+ {letter.terms.map(term => ( + + ))} +
- {/* Badges & Interactions */} -
- - - - - -
-
- - {views} -
- - - -
-
-
-
- ); -} - -export function Badge({ - type, - value, - className = "", -}: { - type: "priority" | "category" | "status"; - value: string; - className?: string; -}) { - return ( - value && ( - - {value?.toUpperCase()} - - ) - ); +
+ + +
+
+
+
+ ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx index 419d7be..e2113b0 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx @@ -1,14 +1,12 @@ import { PostDto, VisitType } from "@nice/common"; import { useVisitor } from "@nice/client"; -import { useContext } from "react"; -import { PostDetailContext } from "../context/PostDetailContext"; import { Button, Tooltip } from "antd"; import { LikeFilled, LikeOutlined } from "@ant-design/icons"; +import { useAuth } from "@web/src/providers/auth-provider"; export default function PostLikeButton({ post }: { post: PostDto }) { - const { user } = useContext(PostDetailContext); + const { user } = useAuth(); const { like, unLike } = useVisitor(); - function likeThisPost() { if (!post?.liked) { post.likes += 1; @@ -37,17 +35,14 @@ export default function PostLikeButton({ post }: { post: PostDto }) { ); diff --git a/apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx b/apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx index 9faf548..88bdaf0 100644 --- a/apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx +++ b/apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx @@ -4,6 +4,7 @@ import { api, usePost } from "@nice/client"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; import { PostState, PostType } from "@nice/common"; +import dayjs from "dayjs"; export interface LetterFormData { title: string; @@ -69,18 +70,34 @@ export function LetterFormProvider({ isPublic: data?.isPublic, resources: data.resources?.length ? { - connect: ( - data.resources?.filter(Boolean) || [] - ).map((fileId) => ({ - fileId, - })), - } + connect: ( + data.resources?.filter(Boolean) || [] + ).map((fileId) => ({ + fileId, + })), + } : undefined, }, }); - // console.log(123); + const formattedDateTime = dayjs().format('YYYY-MM-DD HH:mm:ss') + // 创建包含信件编号和提交时间的文本 + const fileContent = `信件编号: ${result.id}\n投递时间: ${formattedDateTime}`; + // 创建包含信件编号和提交时间的Blob对象 + const blob = new Blob([fileContent], { type: 'text/plain' }); + // 创建下载链接 + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = `信件编号-${result.id}.txt`; // 设置下载文件名 + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + + toast.success(`信件投递成功!信件编号已保存到本地,请妥善保管用于进度查询`, { + duration: 5000 // 10秒 + }); navigate(`/${result.id}/detail`, { replace: true }); - toast.success("发送成功!"); form.resetFields(); } catch (error) { console.error("Error submitting form:", error); diff --git a/apps/web/src/components/models/post/list/LetterList.tsx b/apps/web/src/components/models/post/list/LetterList.tsx index 0adfe6f..205e038 100644 --- a/apps/web/src/components/models/post/list/LetterList.tsx +++ b/apps/web/src/components/models/post/list/LetterList.tsx @@ -5,6 +5,7 @@ import { LetterCard } from "../LetterCard"; import { NonVoid } from "@nice/utils"; import { SearchOutlined } from '@ant-design/icons'; import debounce from 'lodash/debounce'; +import { postDetailSelect } from '@nice/common'; export default function LetterList({ params }: { params: NonVoid }) { const [searchText, setSearchText] = useState(''); const [currentPage, setCurrentPage] = useState(1); @@ -20,9 +21,14 @@ export default function LetterList({ params }: { params: NonVoid { + console.log(data) + }, [data]) // Debounced search function const debouncedSearch = useMemo( () => @@ -73,7 +79,7 @@ export default function LetterList({ params }: { params: NonVoid ) : data?.items.length ? ( <> -
+
{data.items.map((letter: any) => ( ))} diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index fe37e9e..ba34981 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -192,11 +192,7 @@ model Post { state String? // 状态 : 未读、处理中、已回答 title String? // 帖子标题,可为空 content String? // 帖子内容,可为空 - domainId String? @map("domain_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") @@ -208,7 +204,6 @@ model Post { visits Visit[] // 访问记录,关联 Visit 模型 views Int @default(0) likes Int @default(0) - receivers Staff[] @relation("post_receiver") parentId String? @map("parent_id") parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型 diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 668bd06..0e9e0c6 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -12,11 +12,9 @@ export const postDetailSelect: Prisma.PostSelect = { resources: true, createdAt: true, updatedAt: true, - + terms: { - include: { - - }, + select: { id: true, name: true }, }, authorId: true, author: { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index b1d8fa8..c1f8795 100755 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -135,7 +135,7 @@ export type PostDto = Post & { liked: boolean; readedCount: number; commentsCount: number; - term: TermDto; + terms: TermDto[]; author: StaffDto | undefined; receivers: StaffDto[]; resources: Resource[];