add 0126-0947

This commit is contained in:
ditiqi 2025-01-26 09:47:04 +08:00
parent cd25e48617
commit ac18602e58
8 changed files with 323 additions and 286 deletions

View File

@ -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<unknown> }
| 'aggregate'
| 'count'
| 'create'
| 'createMany'
| 'delete'
| 'deleteMany'
| 'findFirst'
| 'findMany'
| 'findUnique'
| 'update'
| 'updateMany'
| 'upsert';
export type DelegateFuncs = {
[K in Operations]: (args: any) => Promise<unknown>;
};
export type DelegateArgs<T> = {
[K in keyof T]: T[K] extends (args: infer A) => Promise<any> ? A : never;
[K in keyof T]: T[K] extends (args: infer A) => Promise<any> ? A : never;
};
export type DelegateReturnTypes<T> = {
[K in keyof T]: T[K] extends (args: any) => Promise<infer R> ? R : never;
[K in keyof T]: T[K] extends (args: any) => Promise<infer R> ? R : never;
};
export type WhereArgs<T> = T extends { where?: infer W } ? W : never;
@ -28,17 +30,17 @@ export type DataArgs<T> = T extends { data: infer D } ? D : never;
export type IncludeArgs<T> = T extends { include: infer I } ? I : never;
export type OrderByArgs<T> = T extends { orderBy: infer O } ? O : never;
export type UpdateOrderArgs = {
id: string
overId: string
}
id: string;
overId: string;
};
export interface FindManyWithCursorType<T extends DelegateFuncs> {
cursor?: string;
limit?: number;
where?: WhereArgs<DelegateArgs<T>['findUnique']>;
select?: SelectArgs<DelegateArgs<T>['findUnique']>;
orderBy?: OrderByArgs<DelegateArgs<T>['findMany']>
cursor?: string;
limit?: number;
where?: WhereArgs<DelegateArgs<T>['findUnique']>;
select?: SelectArgs<DelegateArgs<T>['findUnique']>;
orderBy?: OrderByArgs<DelegateArgs<T>['findMany']>;
}
export type TransactionType = Omit<
PrismaClient,
'$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
>;
PrismaClient,
'$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
>;

View File

@ -12,13 +12,13 @@ const PostDeleteManyArgsSchema: ZodType<Prisma.PostDeleteManyArgs> = z.any();
const PostWhereInputSchema: ZodType<Prisma.PostWhereInput> = z.any();
const PostSelectSchema: ZodType<Prisma.PostSelect> = z.any();
const PostUpdateInputSchema: ZodType<Prisma.PostUpdateInput> = z.any();
const PostOrderBySchema: ZodType<Prisma.PostOrderByWithRelationInput> = z.any()
const PostOrderBySchema: ZodType<Prisma.PostOrderByWithRelationInput> = 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 }) => {

View File

@ -101,22 +101,31 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
});
}
async findManyWithPagination(
args: { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; select?: Prisma.PostSelect<DefaultArgs>; orderBy?: Prisma.PostOrderByWithRelationInput },
args: {
page?: number;
pageSize?: number;
where?: Prisma.PostWhereInput;
select?: Prisma.PostSelect<DefaultArgs>;
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;

View File

@ -12,13 +12,15 @@ const StaffWhereInputSchema: ZodType<Prisma.StaffWhereInput> = z.any();
const StaffSelectSchema: ZodType<Prisma.StaffSelect> = z.any();
const StaffUpdateInputSchema: ZodType<Prisma.PostUpdateInput> = z.any();
const StaffFindManyArgsSchema: ZodType<Prisma.StaffFindManyArgs> = z.any();
const StaffOrderBySchema: ZodType<Prisma.StaffOrderByWithRelationInput> =
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);
}),

View File

@ -105,7 +105,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
* @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<Prisma.StaffDelegate> {
}
}
async findManyWithPagination(args: { page?: number; pageSize?: number; where?: Prisma.StaffWhereInput; select?: Prisma.StaffSelect<DefaultArgs>; }) {
async findManyWithPagination(args: {
page?: number;
pageSize?: number;
where?: Prisma.StaffWhereInput;
select?: Prisma.StaffSelect<DefaultArgs>;
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);
}
}

View File

@ -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<string>();
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<string>();
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 (
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-slate-200">
<WriteHeader term={getTerm(termId)} />
<div className="container mx-auto px-4 py-8">
<div className="mb-8 space-y-4">
{/* Search and Filter Section */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<DepartmentSelect
variant='filled'
size="large"
value={selectedDept}
onChange={setSelectedDept as any}
className="w-1/2"
/>
<Input
variant='filled'
className={'w-1/2'}
prefix={<SearchOutlined className="text-gray-400" />}
placeholder="搜索领导姓名或职级..."
onChange={debounce((e) => setSearchQuery(e.target.value), 300)}
// Reset page when search or department changes
useEffect(() => {
resetPage();
}, [searchQuery, selectedDept, resetPage]);
size="large"
/>
return (
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-slate-200">
<WriteHeader term={getTerm(termId)} />
<div className="container mx-auto px-4 py-8">
<div className="mb-8 space-y-4">
{/* Search and Filter Section */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<DepartmentSelect
variant="filled"
size="large"
value={selectedDept}
onChange={setSelectedDept as any}
className="w-1/2"
/>
<Input
variant="filled"
className={"w-1/2"}
prefix={
<SearchOutlined className="text-gray-400" />
}
placeholder="搜索领导姓名或职级..."
onChange={debounce(
(e) => setSearchQuery(e.target.value),
300
)}
size="large"
/>
</div>
</div>
{error && (
<Alert
message="加载失败"
description="获取数据时出现错误,请刷新页面重试。"
type="error"
showIcon
/>
)}
</div>
{error && (
<Alert
message="加载失败"
description="获取数据时出现错误,请刷新页面重试。"
type="error"
showIcon
/>
)}
</div>
<AnimatePresence>
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Spin size="large" tip="加载中..." />
</div>
) : data?.items.length > 0 ? (
<motion.div
className="grid grid-cols-1 gap-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}>
{data?.items.map((item: any) => (
<SendCard
key={item.id}
staff={item}
termId={termId || undefined}
/>
))}
</motion.div>
) : (
<motion.div
className="text-center py-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}>
<Empty
description="没有找到匹配的收信人"
className="py-12"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Spin size="large" tip="加载中..." />
</div>
) : data?.items.length > 0 ? (
<motion.div
className="grid grid-cols-1 gap-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{data?.items.map((item: any) => (
<SendCard
key={item.id}
staff={item}
termId={termId || undefined}
/>
))}
</motion.div>
) : (
<motion.div
className="text-center py-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Empty
description="没有找到匹配的收信人"
className="py-12"
/>
</motion.div>
)}
</AnimatePresence>
{/* Pagination */}
{data?.items.length > 0 && (
<div className="flex justify-center mt-8">
<Pagination
current={currentPage}
total={data?.totalPages || 0}
pageSize={pageSize}
onChange={(page) => {
setCurrentPage(page);
window.scrollTo(0, 0);
}}
showSizeChanger={false}
showTotal={(total) => `${total} 条记录`}
/>
</div>
)}
</div>
</div>
);
{/* Pagination */}
{data?.items.length > 0 && (
<div className="flex justify-center mt-8">
<Pagination
current={currentPage}
total={data?.totalPages || 0}
pageSize={pageSize}
onChange={(page) => {
setCurrentPage(page);
window.scrollTo(0, 0);
}}
showSizeChanger={false}
showTotal={(total) => `${total} 条记录`}
/>
</div>
)}
</div>
</div>
);
}

View File

@ -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<RouterInputs["post"]["findManyWithPagination"]> }) {
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<RouterInputs["post"]["findManyWithPagination"]>;
}) {
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 (
<div className="flex flex-col h-full">
{/* Search Bar */}
<div className="p-6 transition-all ">
<Input
variant="filled"
className='w-full'
placeholder="搜索信件标题..."
allowClear
size="large"
onChange={(e) => handleSearch(e.target.value)}
prefix={<SearchOutlined className="text-gray-400" />}
/>
</div>
return (
<div className="flex flex-col h-full">
{/* Search Bar */}
<div className="p-6 transition-all ">
<Input
variant="filled"
className="w-full"
placeholder="搜索信件标题..."
allowClear
size="large"
onChange={(e) => handleSearch(e.target.value)}
prefix={<SearchOutlined className="text-gray-400" />}
/>
</div>
{/* Content Area */}
<div className="flex-grow px-6">
{isLoading ? (
<div className='flex justify-center items-center pt-6'>
<Spin size='large'></Spin>
</div>
) : data?.items.length ? (
<>
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-4 mb-6">
{data.items.map((letter: any) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
<div className="flex justify-center pb-6">
<Pagination
current={currentPage}
total={data.totalCount}
pageSize={params.pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
/>
</div>
</>
) : (
<div className="flex flex-col justify-center items-center pt-6">
<Empty
description={
searchText ? "未找到相关信件" : "暂无信件"
}
/>
</div>
)}
</div>
</div>
);
{/* Content Area */}
<div className="flex-grow px-6">
{isLoading ? (
<div className="flex justify-center items-center pt-6">
<Spin size="large"></Spin>
</div>
) : data?.items.length ? (
<>
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-4 mb-6">
{data.items.map((letter: any) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
<div className="flex justify-center pb-6">
<Pagination
current={currentPage}
total={data.totalCount}
pageSize={params.pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
/>
</div>
</>
) : (
<div className="flex flex-col justify-center items-center pt-6">
<Empty
description={
searchText ? "未找到相关信件" : "暂无信件"
}
/>
</div>
)}
</div>
</div>
);
}

View File

@ -8,6 +8,7 @@
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"dev-static": "tsup --no-watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit"
},