From a0447eab2f97452404e56b948048ac1b24d8d2d4 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 09:28:38 +0800 Subject: [PATCH 1/4] add --- apps/server/src/models/base/base.service.ts | 10 +- apps/web/src/app/main/letter/inbox/Header.tsx | 74 ++++ apps/web/src/app/main/letter/inbox/page.tsx | 24 ++ apps/web/src/app/main/letter/index/page.tsx | 11 + .../web/src/app/main/letter/outbox/Header.tsx | 74 ++++ apps/web/src/app/main/letter/outbox/page.tsx | 20 ++ apps/web/src/app/main/letter/write/page.tsx | 1 + .../components/layout/element/usermenu.tsx | 324 +++++++++--------- .../src/components/layout/main/navigation.tsx | 170 ++++----- .../src/components/layout/main/useNavItem.tsx | 123 ++++--- apps/web/src/routes/index.tsx | 15 + 11 files changed, 552 insertions(+), 294 deletions(-) create mode 100644 apps/web/src/app/main/letter/inbox/Header.tsx create mode 100644 apps/web/src/app/main/letter/inbox/page.tsx create mode 100644 apps/web/src/app/main/letter/index/page.tsx create mode 100644 apps/web/src/app/main/letter/outbox/Header.tsx create mode 100644 apps/web/src/app/main/letter/outbox/page.tsx diff --git a/apps/server/src/models/base/base.service.ts b/apps/server/src/models/base/base.service.ts index 19fa918..9367d37 100644 --- a/apps/server/src/models/base/base.service.ts +++ b/apps/server/src/models/base/base.service.ts @@ -38,7 +38,7 @@ export class BaseService< protected prisma: PrismaClient, protected objectType: string, protected enableOrder: boolean = false, - ) { } + ) {} /** * Retrieves the name of the model dynamically. @@ -451,7 +451,11 @@ export class BaseService< pageSize?: number; where?: WhereArgs; select?: SelectArgs; - }): Promise<{ items: R['findMany']; totalPages: number, totalCount: number }> { + }): Promise<{ + items: R['findMany']; + totalPages: number; + totalCount: number; + }> { const { page = 1, pageSize = 10, where, select } = args; try { @@ -470,7 +474,7 @@ export class BaseService< return { items, totalPages, - totalCount: total + totalCount: total, }; } catch (error) { this.handleError(error, 'read'); diff --git a/apps/web/src/app/main/letter/inbox/Header.tsx b/apps/web/src/app/main/letter/inbox/Header.tsx new file mode 100644 index 0000000..3189c55 --- /dev/null +++ b/apps/web/src/app/main/letter/inbox/Header.tsx @@ -0,0 +1,74 @@ +export function Header() { + return ( +
+
+ {/* 主标题区域 */} +
+

+ 我收到的信件 +

+

+ 及时查看 • 快速处理 • 高效反馈 +

+
+ + {/* 服务特点说明 */} +
+
+ + + + 随时查看收到的信件 +
+
+ + + + 快速处理信件内容 +
+
+ + + + 高效反馈处理结果 +
+
+ + {/* 服务宗旨说明 */} +
+

+ 我们确保您能够及时查看、快速处理收到的信件, + 并通过高效反馈机制,提升沟通效率,助力工作顺利开展。 +

+
+
+
+ ); +} diff --git a/apps/web/src/app/main/letter/inbox/page.tsx b/apps/web/src/app/main/letter/inbox/page.tsx new file mode 100644 index 0000000..78c227e --- /dev/null +++ b/apps/web/src/app/main/letter/inbox/page.tsx @@ -0,0 +1,24 @@ +import LetterList from "@web/src/components/models/post/list/LetterList"; +import { Header } from "./Header"; +import { useAuth } from "@web/src/providers/auth-provider"; +export default function InboxPage() { + const { user } = useAuth(); + return ( + // 添加 flex flex-col 使其成为弹性布局容器 +
+
+ {/* 添加 flex-grow 使内容区域自动填充剩余空间 */} + + +
+ ); +} diff --git a/apps/web/src/app/main/letter/index/page.tsx b/apps/web/src/app/main/letter/index/page.tsx new file mode 100644 index 0000000..3dcea41 --- /dev/null +++ b/apps/web/src/app/main/letter/index/page.tsx @@ -0,0 +1,11 @@ +import { useAuth } from "@web/src/providers/auth-provider"; +import InboxPage from "../inbox/page"; +import LetterListPage from "../list/page"; + +export default function IndexPage() { + const { user } = useAuth(); + if (user) { + return ; + } + return ; +} diff --git a/apps/web/src/app/main/letter/outbox/Header.tsx b/apps/web/src/app/main/letter/outbox/Header.tsx new file mode 100644 index 0000000..31630bb --- /dev/null +++ b/apps/web/src/app/main/letter/outbox/Header.tsx @@ -0,0 +1,74 @@ +export function Header() { + return ( +
+
+ {/* 主标题区域 */} +
+

+ 我发出的信件 +

+

+ 清晰记录 • 实时跟踪 • 高效沟通 +

+
+ + {/* 服务特点说明 */} +
+
+ + + + 清晰记录发出的信件 +
+
+ + + + 实时跟踪信件状态 +
+
+ + + + 高效沟通信件进展 +
+
+ + {/* 服务宗旨说明 */} +
+

+ 我们确保您能够清晰记录发出的信件, + 实时跟踪信件状态,并通过高效沟通机制,确保信件处理顺利进行。 +

+
+
+
+ ); +} diff --git a/apps/web/src/app/main/letter/outbox/page.tsx b/apps/web/src/app/main/letter/outbox/page.tsx new file mode 100644 index 0000000..c39748a --- /dev/null +++ b/apps/web/src/app/main/letter/outbox/page.tsx @@ -0,0 +1,20 @@ +import LetterList from "@web/src/components/models/post/list/LetterList"; +import { Header } from "./Header"; +import { useAuth } from "@web/src/providers/auth-provider"; +export default function OutboxPage() { + const { user } = useAuth(); + return ( + // 添加 flex flex-col 使其成为弹性布局容器 +
+
+ {/* 添加 flex-grow 使内容区域自动填充剩余空间 */} + + +
+ ); +} diff --git a/apps/web/src/app/main/letter/write/page.tsx b/apps/web/src/app/main/letter/write/page.tsx index cb8ffba..3a0cf33 100644 --- a/apps/web/src/app/main/letter/write/page.tsx +++ b/apps/web/src/app/main/letter/write/page.tsx @@ -22,6 +22,7 @@ export default function WriteLetterPage() { const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({ page: currentPage, pageSize, + where: { deptId: selectedDept, OR: [{ diff --git a/apps/web/src/components/layout/element/usermenu.tsx b/apps/web/src/components/layout/element/usermenu.tsx index e261666..208dc34 100644 --- a/apps/web/src/components/layout/element/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu.tsx @@ -4,194 +4,196 @@ import { motion, AnimatePresence } from "framer-motion"; import { useState, useRef, useCallback, useMemo } from "react"; import { Avatar } from "../../common/element/Avatar"; import { - UserOutlined, - SettingOutlined, - QuestionCircleOutlined, - LogoutOutlined + UserOutlined, + SettingOutlined, + QuestionCircleOutlined, + LogoutOutlined, } from "@ant-design/icons"; import { Spin } from "antd"; import { useNavigate } from "react-router-dom"; import { MenuItemType } from "./types"; const menuVariants = { - hidden: { opacity: 0, scale: 0.95, y: -10 }, - visible: { - opacity: 1, - scale: 1, - y: 0, - transition: { - type: "spring", - stiffness: 300, - damping: 30 - } - }, - exit: { - opacity: 0, - scale: 0.95, - y: -10, - transition: { - duration: 0.2 - } - } + hidden: { opacity: 0, scale: 0.95, y: -10 }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: { + type: "spring", + stiffness: 300, + damping: 30, + }, + }, + exit: { + opacity: 0, + scale: 0.95, + y: -10, + transition: { + duration: 0.2, + }, + }, }; export function UserMenu() { - const [showMenu, setShowMenu] = useState(false); - const menuRef = useRef(null); - const { user, logout, isLoading } = useAuth(); - const navigate = useNavigate() - useClickOutside(menuRef, () => setShowMenu(false)); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + const { user, logout, isLoading } = useAuth(); + const navigate = useNavigate(); + useClickOutside(menuRef, () => setShowMenu(false)); - const toggleMenu = useCallback(() => { - setShowMenu(prev => !prev); - }, []); + const toggleMenu = useCallback(() => { + setShowMenu((prev) => !prev); + }, []); - const menuItems: MenuItemType[] = useMemo(() => [ - { - icon: , - label: '个人信息', - action: () => { }, - }, - { - icon: , - label: '设置', - action: () => { - navigate('/admin/staff') - }, - }, - { - icon: , - label: '帮助', - action: () => { }, - }, - { - icon: , - label: '注销', - action: () => logout(), - }, - ], [logout]); + const menuItems: MenuItemType[] = useMemo( + () => [ + { + icon: , + label: "个人信息", + action: () => {}, + }, + { + icon: , + label: "设置", + action: () => { + navigate("/admin/staff"); + }, + }, + // { + // icon: , + // label: '帮助', + // action: () => { }, + // }, + { + icon: , + label: "注销", + action: () => logout(), + }, + ], + [logout] + ); - const handleMenuItemClick = useCallback((action: () => void) => { - action(); - setShowMenu(false); - }, []); + const handleMenuItemClick = useCallback((action: () => void) => { + action(); + setShowMenu(false); + }, []); - if (isLoading) { - return ( -
- -
- ); - } + if (isLoading) { + return ( +
+ +
+ ); + } - return ( -
- + - + - + aria-hidden="true" + /> + - - {showMenu && ( - - {/* User Profile Section */} -
+ {/* User Profile Section */} +
+
+ +
+ + {user?.showname || user?.username} + + + + 在线 + +
+
+
- > -
- -
- - {user?.showname || user?.username} - - - - 在线 - -
-
-
- - {/* Menu Items */} -
- {menuItems.map((item, index) => ( - - ))} -
-
- )} -
-
- ); + group-hover:translate-x-0.5 ${ + item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> + {item.icon} + + {item.label} + + ))} + + + )} + + + ); } diff --git a/apps/web/src/components/layout/main/navigation.tsx b/apps/web/src/components/layout/main/navigation.tsx index 1a1cccd..27a1302 100644 --- a/apps/web/src/components/layout/main/navigation.tsx +++ b/apps/web/src/components/layout/main/navigation.tsx @@ -1,96 +1,108 @@ import { NavLink, useLocation } from "react-router-dom"; import { useNavItem } from "./useNavItem"; import { twMerge } from "tailwind-merge"; +import React from "react"; interface NavItem { - to: string; - label: string; - icon?: React.ReactNode; + to: string; + label: string; + icon?: React.ReactNode; } interface NavigationProps { - className?: string; + className?: string; } export default function Navigation({ className }: NavigationProps) { - const { navItems } = useNavItem(); - const location = useLocation(); + const { navItems } = useNavItem(); + const location = useLocation(); - const isActive = (to: string) => { - const [pathname, search] = to.split('?'); - return location.pathname === pathname && - (!search ? !location.search : location.search === `?${search}`); - }; + const isActive = (to: string) => { + const [pathname, search] = to.split("?"); + return ( + location.pathname === pathname && + (!search ? !location.search : location.search === `?${search}`) + ); + }; - return ( - - ); + {/* Mobile Navigation */} +
+
+ {navItems.map((item) => ( + + twMerge( + "px-3 py-1.5 text-sm font-medium rounded-full", + "transition-colors duration-200", + "text-gray-300 hover:text-white", + active && "bg-blue-500/20 text-white" + ) + }> + + {item.icon} + {item.label} + + + ))} +
+
+ + + ); } diff --git a/apps/web/src/components/layout/main/useNavItem.tsx b/apps/web/src/components/layout/main/useNavItem.tsx index c04a818..48f12f7 100644 --- a/apps/web/src/components/layout/main/useNavItem.tsx +++ b/apps/web/src/components/layout/main/useNavItem.tsx @@ -1,66 +1,87 @@ import { api } from "@nice/client"; import { TaxonomySlug } from "@nice/common"; -import { useMemo } from "react"; +import React, { useMemo } from "react"; +import { MailOutlined, SendOutlined } from "@ant-design/icons"; import { - FileTextOutlined, - ScheduleOutlined, - QuestionCircleOutlined, - FolderOutlined, - TagsOutlined + FileTextOutlined, + ScheduleOutlined, + QuestionCircleOutlined, + FolderOutlined, + TagsOutlined, } from "@ant-design/icons"; +import { useAuth } from "@web/src/providers/auth-provider"; export interface NavItem { - to: string; - label: string; - icon?: React.ReactNode; + to: string; + label: string; + icon?: React.ReactNode; } export function useNavItem() { - const { data } = api.term.findMany.useQuery({ - where: { - taxonomy: { slug: TaxonomySlug.CATEGORY } - } - }); + const { user } = useAuth(); + const { data } = api.term.findMany.useQuery({ + where: { + taxonomy: { slug: TaxonomySlug.CATEGORY }, + }, + }); - const navItems = useMemo(() => { - // 定义固定的导航项 - const staticItems = { - letterList: { - to: "/", - label: "公开信件", - icon: - }, - letterProgress: { - to: "/letter-progress", - label: "进度查询", - icon: - }, - help: { - to: "/help", - label: "使用帮助", - icon: - } - }; + const navItems = useMemo(() => { + // 定义固定的导航项 + const staticItems = { + inbox: { + to: user ? "/" : "/inbox", + label: "我收到的", + icon: , + }, + outbox: { + to: "/outbox", + label: "我发出的", + icon: , + }, + letterList: { + to: !user ? "/" : "/letter-list", + label: "公开信件", + icon: , + }, + letterProgress: { + to: "/letter-progress", + label: "进度查询", + icon: , + }, + // help: { + // to: "/help", + // label: "使用帮助", + // icon: + // } + }; - if (!data) { - return [staticItems.letterList, staticItems.letterProgress, staticItems.help]; - } + if (!data) { + return [ + user && staticItems.inbox, + user && staticItems.outbox, + staticItems.letterList, + staticItems.letterProgress, + // staticItems.help, + ].filter(Boolean); + } - // 构建分类导航项 - const categoryItems = data.map(term => ({ - to: `/write-letter?termId=${term.id}`, - label: term.name, - icon: - })); + // 构建分类导航项 + const categoryItems = data.map((term) => ({ + to: `/write-letter?termId=${term.id}`, + label: term.name, + icon: , + })); - // 按照指定顺序返回导航项 - return [ - staticItems.letterList, - staticItems.letterProgress, - ...categoryItems, - staticItems.help - ]; - }, [data]); + // 按照指定顺序返回导航项 + return [ + user && staticItems.inbox, + user && staticItems.outbox, + staticItems.letterList, + staticItems.letterProgress, + ...categoryItems, + // staticItems.help, + ].filter(Boolean); + }, [data]); - return { navItems }; + return { navItems }; } diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index a876714..dc0dca8 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -23,6 +23,9 @@ import LetterDetailPage from "../app/main/letter/detail/page"; import AdminLayout from "../components/layout/admin/AdminLayout"; import { CustomRouteObject } from "./types"; import { adminRoute } from "./admin-route"; +import InboxPage from "../app/main/letter/inbox/page"; +import OutboxPage from "../app/main/letter/outbox/page"; +import IndexPage from "../app/main/letter/index/page"; export const routes: CustomRouteObject[] = [ { path: "/", @@ -36,8 +39,20 @@ export const routes: CustomRouteObject[] = [ { element: , children: [ + { + path: "inbox", + element: , + }, + { + path: "outbox", + element: , + }, { index: true, + element: , + }, + { + path: "letter-list", element: , }, { From ac18602e58fa110fde23873df9912e79348b1c6f Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 09:47:04 +0800 Subject: [PATCH 2/4] add 0126-0947 --- apps/server/src/models/base/base.type.ts | 56 ++-- apps/server/src/models/post/post.router.ts | 6 +- apps/server/src/models/post/post.service.ts | 31 +- apps/server/src/models/staff/staff.router.ts | 19 +- apps/server/src/models/staff/staff.service.ts | 23 +- apps/web/src/app/main/letter/write/page.tsx | 264 +++++++++--------- .../models/post/list/LetterList.tsx | 209 +++++++------- packages/utils/package.json | 1 + 8 files changed, 323 insertions(+), 286 deletions(-) diff --git a/apps/server/src/models/base/base.type.ts b/apps/server/src/models/base/base.type.ts index 878dffe..01254df 100644 --- a/apps/server/src/models/base/base.type.ts +++ b/apps/server/src/models/base/base.type.ts @@ -1,25 +1,27 @@ -import { db, Prisma, PrismaClient } from "@nice/common"; +import { db, Prisma, PrismaClient } from '@nice/common'; export type Operations = - | 'aggregate' - | 'count' - | 'create' - | 'createMany' - | 'delete' - | 'deleteMany' - | 'findFirst' - | 'findMany' - | 'findUnique' - | 'update' - | 'updateMany' - | 'upsert'; -export type DelegateFuncs = { [K in Operations]: (args: any) => Promise } + | 'aggregate' + | 'count' + | 'create' + | 'createMany' + | 'delete' + | 'deleteMany' + | 'findFirst' + | 'findMany' + | 'findUnique' + | 'update' + | 'updateMany' + | 'upsert'; +export type DelegateFuncs = { + [K in Operations]: (args: any) => Promise; +}; export type DelegateArgs = { - [K in keyof T]: T[K] extends (args: infer A) => Promise ? A : never; + [K in keyof T]: T[K] extends (args: infer A) => Promise ? A : never; }; export type DelegateReturnTypes = { - [K in keyof T]: T[K] extends (args: any) => Promise ? R : never; + [K in keyof T]: T[K] extends (args: any) => Promise ? R : never; }; export type WhereArgs = T extends { where?: infer W } ? W : never; @@ -28,17 +30,17 @@ export type DataArgs = T extends { data: infer D } ? D : never; export type IncludeArgs = T extends { include: infer I } ? I : never; export type OrderByArgs = T extends { orderBy: infer O } ? O : never; export type UpdateOrderArgs = { - id: string - overId: string -} + id: string; + overId: string; +}; export interface FindManyWithCursorType { - cursor?: string; - limit?: number; - where?: WhereArgs['findUnique']>; - select?: SelectArgs['findUnique']>; - orderBy?: OrderByArgs['findMany']> + cursor?: string; + limit?: number; + where?: WhereArgs['findUnique']>; + select?: SelectArgs['findUnique']>; + orderBy?: OrderByArgs['findMany']>; } export type TransactionType = Omit< - PrismaClient, - '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' ->; \ No newline at end of file + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index c536977..e94dd57 100755 --- a/apps/server/src/models/post/post.router.ts +++ b/apps/server/src/models/post/post.router.ts @@ -12,13 +12,13 @@ const PostDeleteManyArgsSchema: ZodType = z.any(); const PostWhereInputSchema: ZodType = z.any(); const PostSelectSchema: ZodType = z.any(); const PostUpdateInputSchema: ZodType = z.any(); -const PostOrderBySchema: ZodType = z.any() +const PostOrderBySchema: ZodType = z.any(); @Injectable() export class PostRouter { constructor( private readonly trpc: TrpcService, private readonly postService: PostService, - ) { } + ) {} router = this.trpc.router({ create: this.trpc.protectProcedure .input(PostCreateArgsSchema) @@ -104,7 +104,7 @@ export class PostRouter { pageSize: z.number().optional(), where: PostWhereInputSchema.optional(), select: PostSelectSchema.optional(), - orderBy: PostOrderBySchema.optional() + orderBy: PostOrderBySchema.optional(), }), ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input, ctx }) => { diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index c599add..089f686 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -101,22 +101,31 @@ export class PostService extends BaseService { }); } async findManyWithPagination( - args: { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; select?: Prisma.PostSelect; orderBy?: Prisma.PostOrderByWithRelationInput }, + args: { + page?: number; + pageSize?: number; + where?: Prisma.PostWhereInput; + select?: Prisma.PostSelect; + orderBy?: Prisma.PostOrderByWithRelationInput; + }, 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 as any), 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 }; - }); + return this.wrapResult( + super.findManyWithPagination(args as any), + 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; diff --git a/apps/server/src/models/staff/staff.router.ts b/apps/server/src/models/staff/staff.router.ts index c8d73dd..80e5bdc 100755 --- a/apps/server/src/models/staff/staff.router.ts +++ b/apps/server/src/models/staff/staff.router.ts @@ -12,13 +12,15 @@ const StaffWhereInputSchema: ZodType = z.any(); const StaffSelectSchema: ZodType = z.any(); const StaffUpdateInputSchema: ZodType = z.any(); const StaffFindManyArgsSchema: ZodType = z.any(); +const StaffOrderBySchema: ZodType = + z.any(); @Injectable() export class StaffRouter { constructor( private readonly trpc: TrpcService, private readonly staffService: StaffService, private readonly staffRowService: StaffRowService, - ) { } + ) {} router = this.trpc.router({ create: this.trpc.procedure @@ -78,12 +80,15 @@ export class StaffRouter { return this.staffService.updateOrder(input); }), findManyWithPagination: this.trpc.procedure - .input(z.object({ - page: z.number(), - pageSize: z.number().optional(), - where: StaffWhereInputSchema.optional(), - select: StaffSelectSchema.optional() - })) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .input( + z.object({ + page: z.number(), + pageSize: z.number().optional(), + where: StaffWhereInputSchema.optional(), + select: StaffSelectSchema.optional(), + orderBy: StaffOrderBySchema.optional(), + }), + ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input }) => { return await this.staffService.findManyWithPagination(input); }), diff --git a/apps/server/src/models/staff/staff.service.ts b/apps/server/src/models/staff/staff.service.ts index a2d69fe..1d5850a 100755 --- a/apps/server/src/models/staff/staff.service.ts +++ b/apps/server/src/models/staff/staff.service.ts @@ -105,7 +105,7 @@ export class StaffService extends BaseService { * @returns 更新后的员工记录 */ async updateUserDomain(data: { domainId?: string }, staff?: UserProfile) { - let { domainId } = data; + const { domainId } = data; if (staff.domainId !== domainId) { const result = await this.update({ where: { id: staff.id }, @@ -120,14 +120,23 @@ export class StaffService extends BaseService { } } - async findManyWithPagination(args: { page?: number; pageSize?: number; where?: Prisma.StaffWhereInput; select?: Prisma.StaffSelect; }) { + async findManyWithPagination(args: { + page?: number; + pageSize?: number; + where?: Prisma.StaffWhereInput; + select?: Prisma.StaffSelect; + orderBy?: Prisma.StaffOrderByWithRelationInput; + }) { if (args.where.deptId && typeof args.where.deptId === 'string') { - const childDepts = await this.departmentService.getDescendantIds(args.where.deptId, true); + const childDepts = await this.departmentService.getDescendantIds( + args.where.deptId, + true, + ); args.where.deptId = { - in: childDepts - } + in: childDepts, + }; } - - return super.findManyWithPagination(args) + + return super.findManyWithPagination(args as any); } } diff --git a/apps/web/src/app/main/letter/write/page.tsx b/apps/web/src/app/main/letter/write/page.tsx index 39b237d..a9c6d87 100644 --- a/apps/web/src/app/main/letter/write/page.tsx +++ b/apps/web/src/app/main/letter/write/page.tsx @@ -1,139 +1,149 @@ -import { useState, useCallback, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useSearchParams } from 'react-router-dom'; +import { useState, useCallback, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useSearchParams } from "react-router-dom"; -import { SendCard } from './SendCard'; -import { Spin, Empty, Input, Alert, Pagination } from 'antd'; -import { api, useTerm } from '@nice/client'; -import DepartmentSelect from '@web/src/components/models/department/department-select'; -import debounce from 'lodash/debounce'; -import { SearchOutlined } from '@ant-design/icons'; -import WriteHeader from './WriteHeader'; +import { SendCard } from "./SendCard"; +import { Spin, Empty, Input, Alert, Pagination } from "antd"; +import { api, useTerm } from "@nice/client"; +import DepartmentSelect from "@web/src/components/models/department/department-select"; +import debounce from "lodash/debounce"; +import { SearchOutlined } from "@ant-design/icons"; +import WriteHeader from "./WriteHeader"; export default function WriteLetterPage() { - const [searchParams] = useSearchParams(); - const termId = searchParams.get('termId'); - const [searchQuery, setSearchQuery] = useState(''); - const [selectedDept, setSelectedDept] = useState(); - const [currentPage, setCurrentPage] = useState(1); - const pageSize = 10; - const { getTerm } = useTerm() + const [searchParams] = useSearchParams(); + const termId = searchParams.get("termId"); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedDept, setSelectedDept] = useState(); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 10; + const { getTerm } = useTerm(); - const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({ - page: currentPage, - pageSize, - - where: { - deptId: selectedDept, - OR: [{ - showname: { - contains: searchQuery - } - }, { - username: { - contains: searchQuery - } - }] - } - }); + const { data, isLoading, error } = + api.staff.findManyWithPagination.useQuery({ + page: currentPage, + pageSize, + where: { + deptId: selectedDept, + OR: [ + { + showname: { + contains: searchQuery, + }, + }, + { + username: { + contains: searchQuery, + }, + }, + ], + }, + orderBy: { + order: "desc", + }, + // orderBy:{ - const resetPage = useCallback(() => { - setCurrentPage(1); - }, []); + // } + }); - // Reset page when search or department changes - useEffect(() => { - resetPage(); - }, [searchQuery, selectedDept, resetPage]); + const resetPage = useCallback(() => { + setCurrentPage(1); + }, []); - return ( -
- -
-
- {/* Search and Filter Section */} -
- - } - placeholder="搜索领导姓名或职级..." - onChange={debounce((e) => setSearchQuery(e.target.value), 300)} + // Reset page when search or department changes + useEffect(() => { + resetPage(); + }, [searchQuery, selectedDept, resetPage]); - size="large" - /> + return ( +
+ +
+
+ {/* Search and Filter Section */} +
+ + + } + placeholder="搜索领导姓名或职级..." + onChange={debounce( + (e) => setSearchQuery(e.target.value), + 300 + )} + size="large" + /> +
-
+ {error && ( + + )} +
- {error && ( - - )} -
+ + {isLoading ? ( +
+ +
+ ) : data?.items.length > 0 ? ( + + {data?.items.map((item: any) => ( + + ))} + + ) : ( + + + + )} +
- - {isLoading ? ( -
- -
- ) : data?.items.length > 0 ? ( - - {data?.items.map((item: any) => ( - - ))} - - ) : ( - - - - )} -
- - {/* Pagination */} - {data?.items.length > 0 && ( -
- { - setCurrentPage(page); - window.scrollTo(0, 0); - }} - showSizeChanger={false} - showTotal={(total) => `共 ${total} 条记录`} - /> -
- )} -
-
- ); + {/* Pagination */} + {data?.items.length > 0 && ( +
+ { + setCurrentPage(page); + window.scrollTo(0, 0); + }} + showSizeChanger={false} + showTotal={(total) => `共 ${total} 条记录`} + /> +
+ )} +
+
+ ); } diff --git a/apps/web/src/components/models/post/list/LetterList.tsx b/apps/web/src/components/models/post/list/LetterList.tsx index 0e60ef6..b11d055 100644 --- a/apps/web/src/components/models/post/list/LetterList.tsx +++ b/apps/web/src/components/models/post/list/LetterList.tsx @@ -1,113 +1,114 @@ -import { useState, useEffect, useMemo } from 'react'; -import { Input, Pagination, Empty, Spin } from 'antd'; +import { useState, useEffect, useMemo } from "react"; +import { Input, Pagination, Empty, Spin } from "antd"; import { api, RouterInputs } from "@nice/client"; 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); +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); - const { data, isLoading } = api.post.findManyWithPagination.useQuery({ - page: currentPage, - pageSize: params.pageSize, - where: { - OR: [{ - title: { - contains: searchText - } - }], - ...params?.where - }, - orderBy: { - updatedAt: "desc" - }, - select: { - ...postDetailSelect, - ...params.select - } - }); + const { data, isLoading } = api.post.findManyWithPagination.useQuery({ + page: currentPage, + pageSize: params.pageSize, + where: { + OR: [ + { + title: { + contains: searchText, + }, + }, + ], + ...params?.where, + }, + orderBy: { + updatedAt: "desc", + }, + select: { + ...postDetailSelect, + ...params.select, + }, + }); - const debouncedSearch = useMemo( - () => - debounce((value: string) => { - setSearchText(value); - setCurrentPage(1); - }, 300), - [] - ); - // Cleanup debounce on unmount - useEffect(() => { - return () => { - debouncedSearch.cancel(); - }; - }, [debouncedSearch]); - const handleSearch = (value: string) => { - debouncedSearch(value); - }; + const debouncedSearch = useMemo( + () => + debounce((value: string) => { + setSearchText(value); + setCurrentPage(1); + }, 300), + [] + ); + // Cleanup debounce on unmount + useEffect(() => { + return () => { + debouncedSearch.cancel(); + }; + }, [debouncedSearch]); + const handleSearch = (value: string) => { + debouncedSearch(value); + }; - const handlePageChange = (page: number) => { - setCurrentPage(page); - // Scroll to top when page changes - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; + const handlePageChange = (page: number) => { + setCurrentPage(page); + // Scroll to top when page changes + window.scrollTo({ top: 0, behavior: "smooth" }); + }; - return ( -
- {/* Search Bar */} -
- handleSearch(e.target.value)} - prefix={} - /> -
+ return ( +
+ {/* Search Bar */} +
+ handleSearch(e.target.value)} + prefix={} + /> +
- {/* Content Area */} -
- {isLoading ? ( -
- -
- ) : data?.items.length ? ( - <> -
- {data.items.map((letter: any) => ( - - ))} -
-
- -
- - ) : ( -
- -
- )} -
- - -
- ); + {/* Content Area */} +
+ {isLoading ? ( +
+ +
+ ) : data?.items.length ? ( + <> +
+ {data.items.map((letter: any) => ( + + ))} +
+
+ +
+ + ) : ( +
+ +
+ )} +
+
+ ); } diff --git a/packages/utils/package.json b/packages/utils/package.json index 8f3fd0e..c02c2c3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "dev-static": "tsup --no-watch", "clean": "rimraf dist", "typecheck": "tsc --noEmit" }, From f121322bbd2ea72727f6890c3308228f0898d69a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 10:34:44 +0800 Subject: [PATCH 3/4] add --- .../common/uploader/AvatarUploader.tsx | 131 ++++++++++ .../common/uploader/FileUploader.tsx | 241 ------------------ .../components/layout/element/usermenu.tsx | 50 ++-- .../components/models/staff/staff-editor.tsx | 142 ++++++----- .../components/models/staff/staff-form.tsx | 66 +++-- 5 files changed, 290 insertions(+), 340 deletions(-) create mode 100644 apps/web/src/components/common/uploader/AvatarUploader.tsx delete mode 100644 apps/web/src/components/common/uploader/FileUploader.tsx diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx new file mode 100644 index 0000000..6efc6c9 --- /dev/null +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -0,0 +1,131 @@ +import { env } from "@web/src/env"; +import { message, Progress, Spin, theme } from "antd"; +import React, { useState, useEffect, useRef } from "react"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; + +export interface AvatarUploaderProps { + value?: string; + placeholder?: string; + className?: string; + onChange?: (value: string) => void; + style?: React.CSSProperties; // 添加style属性 +} + +interface UploadingFile { + name: string; + progress: number; + status: "uploading" | "done" | "error"; + fileId?: string; + fileKey?: string; +} + +const AvatarUploader: React.FC = ({ + value, + onChange, + className, + placeholder = "点击上传", + style, // 解构style属性 +}) => { + const { handleFileUpload, uploadProgress } = useTusUpload(); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(value || ""); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + + const { token } = theme.useToken(); + + const handleChange = async (event: React.ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (!selectedFile) return; + + setFile({ + name: selectedFile.name, + progress: 0, + status: "uploading", + fileKey: `${selectedFile.name}-${Date.now()}`, + }); + setUploading(true); + + try { + const fileId = await new Promise((resolve, reject) => { + handleFileUpload( + selectedFile, + (result) => { + setFile((prev) => ({ + ...prev!, + progress: 100, + status: "done", + fileId: result.fileId, + })); + resolve(result.fileId); + }, + (error) => { + reject(error); + }, + file?.fileKey + ); + }); + setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`); + onChange?.(fileId); + message.success("头像上传成功"); + } catch (error) { + console.error("上传错误:", error); + message.error("头像上传失败"); + setFile((prev) => ({ ...prev!, status: "error" })); + } finally { + setUploading(false); + } + }; + + const triggerUpload = () => { + inputRef.current?.click(); + }; + + return ( +
+ + {previewUrl ? ( + Avatar + ) : ( +
+ {placeholder} +
+ )} + {uploading && ( +
+ +
+ )} + {file && file.status === "uploading" && ( +
+ +
+ )} +
+ ); +}; + +export default AvatarUploader; diff --git a/apps/web/src/components/common/uploader/FileUploader.tsx b/apps/web/src/components/common/uploader/FileUploader.tsx deleted file mode 100644 index 99bb2af..0000000 --- a/apps/web/src/components/common/uploader/FileUploader.tsx +++ /dev/null @@ -1,241 +0,0 @@ -// FileUploader.tsx -import React, { useRef, memo, useState } from "react"; -import { - CloudArrowUpIcon, - XMarkIcon, - DocumentIcon, - ExclamationCircleIcon, - CheckCircleIcon, -} from "@heroicons/react/24/outline"; -import { motion, AnimatePresence } from "framer-motion"; -import { toast } from "react-hot-toast"; -import { useTusUpload } from "@web/src/hooks/useTusUpload"; - -interface FileUploaderProps { - endpoint?: string; - onSuccess?: (result: { url: string; fileId: string }) => void; - onError?: (error: Error) => void; - maxSize?: number; - allowedTypes?: string[]; - placeholder?: string; -} - -interface FileItemProps { - file: File; - progress?: number; - onRemove: (name: string) => void; - isUploaded: boolean; -} - -const FileItem: React.FC = memo( - ({ file, progress, onRemove, isUploaded }) => ( - - -
-
-

- {file.name} -

- -
- {!isUploaded && progress !== undefined && ( -
-
- -
- - {progress}% - -
- )} - {isUploaded && ( -
- - 上传完成 -
- )} -
-
- ) -); - -const FileUploader: React.FC = ({ - onSuccess, - onError, - maxSize = 100, - placeholder = "点击或拖拽文件到这里上传", - allowedTypes = ["*/*"], -}) => { - const [isDragging, setIsDragging] = useState(false); - const [files, setFiles] = useState< - Array<{ file: File; isUploaded: boolean }> - >([]); - const fileInputRef = useRef(null); - - const { progress, isUploading, uploadError, handleFileUpload } = - useTusUpload(); - - const handleError = (error: Error) => { - toast.error(error.message); - onError?.(error); - }; - - const handleDrag = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.type === "dragenter" || e.type === "dragover") { - setIsDragging(true); - } else if (e.type === "dragleave") { - setIsDragging(false); - } - }; - - const validateFile = (file: File) => { - if (file.size > maxSize * 1024 * 1024) { - throw new Error(`文件大小不能超过 ${maxSize}MB`); - } - if ( - !allowedTypes.includes("*/*") && - !allowedTypes.includes(file.type) - ) { - throw new Error( - `不支持的文件类型。支持的类型: ${allowedTypes.join(", ")}` - ); - } - }; - - const uploadFile = (file: File) => { - try { - validateFile(file); - handleFileUpload( - file, - (upload) => { - console.log("Upload complete:", { - url: upload.url, - fileId: upload.fileId, - // resource: upload.resource - }); - onSuccess?.(upload); - setFiles((prev) => - prev.map((f) => - f.file.name === file.name - ? { ...f, isUploaded: true } - : f - ) - ); - }, - handleError - ); - } catch (error) { - handleError(error as Error); - } - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const droppedFiles = Array.from(e.dataTransfer.files); - setFiles((prev) => [ - ...prev, - ...droppedFiles.map((file) => ({ file, isUploaded: false })), - ]); - droppedFiles.forEach(uploadFile); - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files); - setFiles((prev) => [ - ...prev, - ...selectedFiles.map((file) => ({ file, isUploaded: false })), - ]); - selectedFiles.forEach(uploadFile); - } - }; - - const removeFile = (fileName: string) => { - setFiles((prev) => prev.filter(({ file }) => file.name !== fileName)); - }; - - const handleClick = () => { - fileInputRef.current?.click(); - }; - - return ( -
-
- - -

{placeholder}

- {isDragging && ( -
-

- 释放文件以上传 -

-
- )} -
- - -
- {files.map(({ file, isUploaded }) => ( - - ))} -
-
- - {uploadError && ( -
- - {uploadError} -
- )} -
- ); -}; - -export default FileUploader; diff --git a/apps/web/src/components/layout/element/usermenu.tsx b/apps/web/src/components/layout/element/usermenu.tsx index 208dc34..6363dfb 100644 --- a/apps/web/src/components/layout/element/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu.tsx @@ -96,24 +96,38 @@ export function UserMenu() { whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={toggleMenu} - className="relative rounded-full focus:outline-none - focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2 - focus:ring-offset-white transition-all duration-200 ease-in-out"> - -