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 009b577..de28fad 100644 --- a/apps/web/src/components/models/post/LetterCard.tsx +++ b/apps/web/src/components/models/post/LetterCard.tsx @@ -9,12 +9,10 @@ import { 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 { @@ -22,78 +20,104 @@ interface LetterCardProps { } export function LetterCard({ letter }: LetterCardProps) { - - return ( -
-
- {/* Title & Priority */} +
{ + 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 " + > +
- + <Title level={4} className="!mb-0 flex-1 font-serif tracking-tight text-gray-800"> <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"> + + className="text-primary hover:text-primary-600 transition-colors duration-200 hover:underline"> {letter.title} </a>
- {/* Meta Info */} -
- - - - +
+
+
+ + {letter.author?.showname || '匿名用户'} - - - - {letter.receivers.map(item => item.department?.name).toString()} - - - - - {letter.receivers.map(item => item.showname).toString()} - - - - - - +
+ + {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 && ' 等'} + + +
+ )} + + {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} - -
+ + {letter.content} + )} - +
+ + {letter.meta.tags.map(tag => ( + + ))} +
{/* Badges & Interactions */} -
- - - - +
+
+ {letter.terms.map(term => ( + + ))} +
-
- - {letter.views} -
+
@@ -101,27 +125,3 @@ export function LetterCard({ letter }: LetterCardProps) {
); } - -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 db34df0..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,7 +1,5 @@ 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"; @@ -37,17 +35,14 @@ export default function PostLikeButton({ post }: { post: PostDto }) { ); 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[];