This commit is contained in:
longdayi 2025-01-25 00:37:59 +08:00
parent c1268b1a8f
commit 8cb4ad1c9b
22 changed files with 343 additions and 362 deletions

View File

@ -38,7 +38,7 @@ export class BaseService<
protected prisma: PrismaClient,
protected objectType: string,
protected enableOrder: boolean = false,
) {}
) { }
/**
* Retrieves the name of the model dynamically.
@ -457,6 +457,7 @@ export class BaseService<
try {
// 获取总记录数
const total = (await this.getModel().count({ where })) as number;
// 获取分页数据
const items = (await this.getModel().findMany({
where,

View File

@ -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)
@ -91,5 +91,15 @@ export class PostRouter {
const { staff } = ctx;
return await this.postService.findManyWithCursor(input, staff);
}),
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
.query(async ({ input }) => {
return await this.postService.findManyWithPagination(input);
}),
});
}

View File

@ -11,6 +11,7 @@ import { z } from 'zod';
import { BaseService } from '../base/base.service';
import * as argon2 from 'argon2';
import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { DefaultArgs } from '@prisma/client/runtime/library';
@Injectable()
export class StaffService extends BaseService<Prisma.StaffDelegate> {
@ -119,72 +120,14 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
}
}
// /**
// * 根据关键词或ID集合查找员工
// * @param data 包含关键词、域ID和ID集合的对象
// * @returns 匹配的员工记录列表
// */
// async findMany(data: z.infer<typeof StaffMethodSchema.findMany>) {
// const { keyword, domainId, ids, deptId, limit = 30 } = data;
// const idResults = ids
// ? await db.staff.findMany({
// where: {
// id: { in: ids },
// deletedAt: null,
// domainId,
// deptId,
// },
// select: {
// id: true,
// showname: true,
// username: true,
// deptId: true,
// domainId: true,
// department: true,
// domain: true,
// },
// })
// : [];
// const mainResults = await db.staff.findMany({
// where: {
// deletedAt: null,
// domainId,
// deptId,
// OR: (keyword || ids) && [
// { showname: { contains: keyword } },
// {
// username: {
// contains: keyword,
// },
// },
// { phoneNumber: { contains: keyword } },
// // {
// // id: { in: ids },
// // },
// ],
// },
// select: {
// id: true,
// showname: true,
// username: true,
// deptId: true,
// domainId: true,
// department: true,
// domain: true,
// },
// orderBy: { order: 'asc' },
// take: limit !== -1 ? limit : undefined,
// });
// // Combine results, ensuring no duplicates
// const combinedResults = [
// ...mainResults,
// ...idResults.filter(
// (idResult) =>
// !mainResults.some((mainResult) => mainResult.id === idResult.id),
// ),
// ];
// return combinedResults;
// }
async findManyWithPagination(args: { page?: number; pageSize?: number; where?: Prisma.StaffWhereInput; select?: Prisma.StaffSelect<DefaultArgs>; }) {
if (args.where.deptId && typeof args.where.deptId === 'string') {
const childDepts = await this.departmentService.getDescendantIds(args.where.deptId, true);
args.where.deptId = {
in: childDepts
}
}
return super.findManyWithPagination(args)
}
}

View File

@ -16,6 +16,7 @@ import { RoleRouter } from '@server/models/rbac/role.router';
@Injectable()
export class TrpcRouter {
logger = new Logger(TrpcRouter.name);
constructor(
private readonly trpc: TrpcService,
@ -30,8 +31,11 @@ export class TrpcRouter {
private readonly app_config: AppConfigRouter,
private readonly message: MessageRouter,
private readonly visitor: VisitRouter,
// private readonly websocketService: WebSocketService
) {}
) { }
getRouter() {
return
}
appRouter = this.trpc.router({
transform: this.transform.router,
post: this.post.router,
@ -66,4 +70,3 @@ export class TrpcRouter {
// });
}
}
export type AppRouter = TrpcRouter[`appRouter`];

View File

@ -0,0 +1,3 @@
import { TrpcRouter } from "./trpc.router";
export type AppRouter = TrpcRouter[`appRouter`];

View File

@ -10,7 +10,6 @@
"target": "ES2020",
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"incremental": true,
// "skipLibCheck": true,

View File

@ -1,14 +1,13 @@
import { motion } from 'framer-motion';
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
import { Leader } from './types';
import { StaffDto } from "@nice/common";
import { Button } from 'antd';
interface LeaderCardProps {
leader: Leader;
isSelected: boolean;
onSelect: () => void;
export interface SendCardProps {
staff: StaffDto;
}
export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardProps) {
export function SendCard({ staff }: SendCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -17,20 +16,36 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
transition={{ duration: 0.2 }}
className={`
bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100
${isSelected
? 'ring-2 ring-[#00308F]'
: 'hover:shadow-lg hover:border-blue-100'
}
`}
>
<div className="flex flex-col sm:flex-row">
{/* Image Container */}
<div className="sm:w-48 h-64 sm:h-auto flex-shrink-0">
<img
src={leader.imageUrl}
alt={leader.name}
className="w-full h-full object-cover"
/>
<div className="sm:w-48 h-64 sm:h-auto flex-shrink-0 bg-gray-100 flex items-center justify-center">
{staff.meta?.photoUrl ? (
<img
src={staff.meta.photoUrl}
alt={staff.showname}
className="w-full h-full object-cover"
/>
) : (
<div className="flex flex-col items-center justify-center text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-16 w-16 mb-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span className="text-sm"></span>
</div>
)}
</div>
{/* Content Container */}
@ -39,13 +54,13 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-semibold text-gray-900">
{leader.name}
{staff.showname}
</h3>
<span className="px-3 py-1 text-sm font-medium bg-blue-50 text-primary rounded-full">
{leader.rank}
{staff.meta?.rank || '未设置职级'}
</span>
</div>
<p className="text-gray-600 mb-4">{leader.division}</p>
<p className="text-gray-600 mb-4">{staff.department?.name || '未设置部门'}</p>
{/* Contact Information */}
<div className="space-y-2 text-sm text-gray-600">
@ -54,36 +69,32 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{leader.email}
{staff.meta?.email || '未设置邮箱'}
</p>
<p className="flex items-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
{leader.phone}
{staff.phoneNumber || '未设置电话'}
</p>
<p className="flex items-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
{leader.office}
{staff.meta?.office || '未设置办公室'}
</p>
</div>
</div>
<button
onClick={onSelect}
className="mt-auto w-full sm:w-auto flex items-center justify-center gap-2
bg-primary text-white py-3 px-6 rounded-lg
hover:bg-primary-600 transition-all duration-300
focus:outline-none focus:ring-2 focus:ring-[#00308F] focus:ring-opacity-50
transform hover:-translate-y-0.5"
<Button
type="primary"
size='large'
>
<PaperAirplaneIcon className="w-5 h-5" />
Compose Letter
</button>
</Button>
</div>
</div>
</div>

View File

@ -1,56 +0,0 @@
import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useMemo, useState } from "react";
import { Input, Select } from "antd";
import { motion } from "framer-motion";
import DepartmentSelect from "@web/src/components/models/department/department-select";
const { Search } = Input;
interface FilterProps {
onSearch?: (query: string) => void;
onDivisionChange?: (division: string) => void;
}
export default function Filter({ onSearch, onDivisionChange }: FilterProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedDivision, setSelectedDivision] = useState<string>('all');
const handleSearch = (value: string) => {
setSearchQuery(value);
onSearch?.(value);
};
const handleDivisionChange = (value: string) => {
setSelectedDivision(value);
onDivisionChange?.(value);
};
return (
<motion.div
className="flex flex-col md:flex-row gap-4 mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="flex-1">
<Search
placeholder="Search by name or rank..."
allowClear
enterButton={
<div className="flex items-center gap-2">
<MagnifyingGlassIcon className="w-5 h-5" />
<span>Search</span>
</div>
}
size="large"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="rounded-lg"
/>
</div>
<DepartmentSelect ></DepartmentSelect>
</motion.div>
);
}

View File

@ -1,50 +1,95 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Leader } from './types';
import { leaders } from './mock';
import Header from './header';
import Filter from './filter';
import LeaderCard from './LeaderCard';
import { Spin, Empty } from 'antd';
import { api } from 'packages/client/dist';
import { SendCard } from './SendCard';
import { Spin, Empty, Input, Alert, message, Pagination } from 'antd';
import { api } from '@nice/client';
import DepartmentSelect from '@web/src/components/models/department/department-select';
import debounce from 'lodash/debounce';
import { SearchOutlined } from '@ant-design/icons';
export default function WriteLetterPage() {
const [selectedLeader, setSelectedLeader] = useState<Leader | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedDivision, setSelectedDivision] = useState<string>('all');
const [selectedDept, setSelectedDept] = useState<string>();
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
const filteredLeaders = useMemo(() => {
return leaders.filter(leader => {
const matchesSearch = leader.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
leader.rank.toLowerCase().includes(searchQuery.toLowerCase());
const matchesDivision = selectedDivision === 'all' || leader.division === selectedDivision;
return matchesSearch && matchesDivision;
});
}, [searchQuery, selectedDivision]);
const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({
page: currentPage,
pageSize,
where: {
deptId: selectedDept,
OR: [{
showname: {
contains: searchQuery
}
}, {
username: {
contains: searchQuery
}
}]
}
});
useEffect(() => {
console.log(selectedDept)
console.log(data)
console.log(searchQuery)
}, [selectedDept, data, searchQuery])
// Reset to first page when search query or department changes
useCallback(() => {
setCurrentPage(1);
}, [searchQuery, selectedDept]);
return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100">
<Header />
<div className="container mx-auto px-4 py-8">
<Filter />
<div className="mb-8 space-y-4">
{/* Search and Filter Section */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<div className="w-full md:w-96">
<Input
prefix={<SearchOutlined className="text-gray-400" />}
placeholder="搜索领导姓名或职级..."
onChange={debounce((e) => setSearchQuery(e.target.value), 300)}
className="w-full"
size="large"
/>
</div>
<DepartmentSelect
size="large"
value={selectedDept}
onChange={setSelectedDept as any}
className="w-full md:w-64"
/>
</div>
{error && (
<Alert
message="加载失败"
description="获取数据时出现错误,请刷新页面重试。"
type="error"
showIcon
/>
)}
</div>
<AnimatePresence>
{filteredLeaders.length > 0 ? (
{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 }}
>
{filteredLeaders.map((leader) => (
<LeaderCard
key={leader.id}
leader={leader}
isSelected={selectedLeader?.id === leader.id}
onSelect={() => setSelectedLeader(leader)}
{data?.items.map((item) => (
<SendCard
key={item.id}
staff={item as any}
/>
))}
</motion.div>
@ -55,10 +100,27 @@ export default function WriteLetterPage() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Empty></Empty>
<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)}
showSizeChanger={false}
showTotal={(total) => `${total} 条记录`}
/>
</div>
)}
</div>
</div>
);

View File

@ -14,6 +14,7 @@ interface DepartmentSelectProps {
domain?: boolean;
disabled?: boolean;
className?: string;
size?: "small" | "middle" | "large";
}
export default function DepartmentSelect({
@ -24,6 +25,7 @@ export default function DepartmentSelect({
placeholder = "选择单位",
multiple = false,
rootId = null,
size,
disabled = false,
domain = undefined,
}: DepartmentSelectProps) {
@ -150,6 +152,7 @@ export default function DepartmentSelect({
disabled={disabled}
showSearch
allowClear
size={size}
defaultValue={defaultValue}
value={value}
className={className}

View File

@ -0,0 +1,5 @@
import { api } from "@nice/client";
export default function LetterList(){
}

View File

@ -16,7 +16,7 @@ export function useDepartment() {
const update = api.department.update.useMutation({
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey });
emitDataChange(ObjectType.DEPARTMENT, result as any, CrudOperation.UPDATED)
},
@ -51,34 +51,6 @@ export function useDepartment() {
});
};
// const getTreeData = () => {
// const uniqueData: DepartmentDto[] = getCacheDataFromQuery(
// queryClient,
// api.department
// );
// const treeData: DataNode[] = buildTree(uniqueData);
// return treeData;
// };
// const getTreeData = () => {
// const cacheArray = queryClient.getQueriesData({
// queryKey: getQueryKey(api.department.getChildren),
// });
// const data: DepartmentDto[] = cacheArray
// .flatMap((cache) => cache.slice(1))
// .flat()
// .filter((item) => item !== undefined) as any;
// const uniqueDataMap = new Map<string, DepartmentDto>();
// data?.forEach((item) => {
// if (item && item.id) {
// uniqueDataMap.set(item.id, item);
// }
// });
// // Convert the Map back to an array
// const uniqueData: DepartmentDto[] = Array.from(uniqueDataMap.values());
// const treeData: DataNode[] = buildTree(uniqueData);
// return treeData;
// };
const getDept = <T = DepartmentDto>(key: string) => {
return findQueryData<T>(queryClient, api.department, key);
};

View File

@ -0,0 +1,86 @@
import type { UseTRPCMutationResult } from "@trpc/react-query/shared";
import { api, type RouterInputs, type RouterOutputs } from "../trpc";
/**
* MutationType mutation
* @template T - 'post', 'user'
* @description RouterInputs[T] mutation 'create', 'update'
*/
type MutationType<T extends keyof RouterInputs> = keyof {
[K in keyof RouterInputs[T]]: K extends "create" | "update" | "deleteMany" | "softDeleteByIds" | "restoreByIds" | "updateOrder"
? RouterInputs[T][K]
: never
};
/**
* MutationOptions mutation
* @template T -
* @template K - mutation
* @description onSuccess mutation
*/
type MutationOptions<T extends keyof RouterInputs, K extends MutationType<T>> = {
onSuccess?: (
data: RouterOutputs[T][K], // mutation 成功后的返回数据
variables: RouterInputs[T][K], // mutation 的输入参数
context?: unknown // 可选的上下文信息
) => void;
};
/**
* EntityOptions mutation
* @template T -
* @description mutation MutationOptions
*/
type EntityOptions<T extends keyof RouterInputs> = {
[K in MutationType<T>]?: MutationOptions<T, K>;
};
/**
* UseTRPCMutationResult
* @template T -
* @template K - mutation
* @description UseTRPCMutationResult 使
*/
type MutationResult<T extends keyof RouterInputs, K extends MutationType<T>> = UseTRPCMutationResult<
RouterOutputs[T][K], // mutation 成功后的返回数据
unknown, // mutation 的错误类型
RouterInputs[T][K], // mutation 的输入参数
unknown // mutation 的上下文类型
>;
/**
* Hook mutation
* @template T - 'post', 'user'
* @param {T} key -
* @param {EntityOptions<T>} [options] - mutation
* @returns mutation
* @description Hook mutation create, update, deleteMany mutation
*/
export function useEntity<T extends keyof RouterInputs>(key: T, options?: EntityOptions<T>) {
const utils = api.useUtils(); // 获取 tRPC 的工具函数,用于操作缓存
/**
* mutation
* @template K - mutation
* @param {K} mutation - mutation
* @returns mutation
* @description mutation mutation onSuccess
*/
const createMutationHandler = <K extends MutationType<T>>(mutation: K) => {
const mutationFn = (api[key as any])[mutation]; // 获取对应的 tRPC mutation 函数
return mutationFn.useMutation({
onSuccess: (data, variables, context) => {
utils[key].invalidate(); // 失效指定实体的缓存
options?.[mutation]?.onSuccess?.(data, variables, context); // 调用用户自定义的 onSuccess 回调
},
});
};
// 返回包含多个 mutation 函数的对象
return {
create: createMutationHandler("create") as MutationResult<T, "create">, // 创建实体的 mutation 函数
update: createMutationHandler("update") as MutationResult<T, "create">, // 更新实体的 mutation 函数
deleteMany: createMutationHandler("deleteMany") as MutationResult<T, "deleteMany">, // 批量删除实体的 mutation 函数
softDeleteByIds: createMutationHandler("softDeleteByIds") as MutationResult<T, "softDeleteByIds">, // 软删除实体的 mutation 函数
restoreByIds: createMutationHandler("restoreByIds") as MutationResult<T, "restoreByIds">, // 恢复软删除实体的 mutation 函数
updateOrder: createMutationHandler("updateOrder") as MutationResult<T, "updateOrder">, // 更新实体顺序的 mutation 函数
};
}

View File

@ -1,18 +1,5 @@
import { api } from "../trpc";
import { useQueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
import { Prisma } from "packages/common/dist";
import { useEntity } from "./useEntity";
export function useMessage() {
const queryClient = useQueryClient();
const queryKey = getQueryKey(api.message);
const create:any = api.message.create.useMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
},
});
return {
create
};
}
return useEntity("message")
}

View File

@ -1,37 +1,4 @@
import { api } from "../trpc";
import { useEntity } from "./useEntity";
export function usePost() {
const utils = api.useUtils();
const create: any = api.post.create.useMutation({
onSuccess: () => {
utils.post.invalidate();
},
});
const update: any = api.post.update.useMutation({
onSuccess: () => {
utils.post.invalidate();
},
});
const deleteMany = api.post.deleteMany.useMutation({
onSuccess: () => {
utils.post.invalidate();
},
});
const softDeleteByIds: any = api.post.softDeleteByIds.useMutation({
onSuccess: () => {
utils.post.invalidate();
},
});
const restoreByIds: any = api.post.restoreByIds.useMutation({
onSuccess: () => {
utils.post.invalidate();
},
});
return {
create,
update,
deleteMany,
softDeleteByIds,
restoreByIds,
};
return useEntity("post")
}

View File

@ -1,27 +0,0 @@
import type { SkipToken } from "@tanstack/react-query";
import type { TRPCClientErrorLike } from "@trpc/client";
import type {
UseTRPCQueryOptions,
UseTRPCQueryResult,
} from "@trpc/react-query/shared";
import type { DecoratedQuery } from "node_modules/@trpc/react-query/dist/createTRPCReact";
export const useQueryApi = <
T extends DecoratedQuery<{
input: any;
output: any;
transformer: any;
errorShape: any;
}>,
U extends T extends DecoratedQuery<infer R> ? R : never,
>(
query: T,
input: U["input"] | SkipToken,
opts?: UseTRPCQueryOptions<
U["output"],
U["input"],
TRPCClientErrorLike<U["output"]>,
U["output"]
>,
): UseTRPCQueryResult<U["output"], TRPCClientErrorLike<U>> =>
query.useQuery(input, opts);

View File

@ -3,7 +3,15 @@ import { api } from "../trpc"; // Adjust path as necessary
import { useQueryClient } from "@tanstack/react-query";
import { CrudOperation, emitDataChange, EventBus } from "../../event";
import { ObjectType } from "@nice/common";
export function useRoleMap() {
type RoleMapHookReturn = {
create: ReturnType<typeof api.rolemap.setRoleForObject.useMutation>;
update: ReturnType<typeof api.rolemap.update.useMutation>;
setRoleForObjects: ReturnType<typeof api.rolemap.setRoleForObjects.useMutation>;
deleteMany: ReturnType<typeof api.rolemap.deleteMany.useMutation>;
addRoleForObjects: ReturnType<typeof api.rolemap.addRoleForObjects.useMutation>;
};
export function useRoleMap(): RoleMapHookReturn {
const queryClient = useQueryClient();
const queryKey = getQueryKey(api.rolemap);
@ -30,10 +38,10 @@ export function useRoleMap() {
});
const deleteMany = api.rolemap.deleteMany.useMutation({
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey });
emitDataChange(ObjectType.ROLE_MAP, result as any, CrudOperation.DELETED)
},
});
@ -45,3 +53,4 @@ export function useRoleMap() {
addRoleForObjects
};
}

View File

@ -1,43 +1,29 @@
import { getQueryKey } from "@trpc/react-query";
import { api } from "../trpc"; // Adjust path as necessary
import { useQueryClient } from "@tanstack/react-query";
import { ObjectType, Staff } from "@nice/common";
import { findQueryData } from "../utils";
import { CrudOperation, emitDataChange } from "../../event";
import { useEntity } from "./useEntity";
export function useStaff() {
const queryClient = useQueryClient();
const queryKey = getQueryKey(api.staff);
const create = api.staff.create.useMutation({
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey });
emitDataChange(ObjectType.STAFF, result as any, CrudOperation.CREATED)
},
});
const updateUserDomain = api.staff.updateUserDomain.useMutation({
onSuccess: async (result) => {
queryClient.invalidateQueries({ queryKey });
},
});
const update = api.staff.update.useMutation({
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey });
emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED)
},
});
const softDeleteByIds = api.staff.softDeleteByIds.useMutation({
onSuccess: (result, variables) => {
queryClient.invalidateQueries({ queryKey });
},
});
const queryClient = useQueryClient()
const getStaff = (key: string) => {
return findQueryData<Staff>(queryClient, api.staff, key);
};
return {
create,
update,
softDeleteByIds,
getStaff,
updateUserDomain
};
...useEntity("staff", {
create: {
onSuccess(result) {
emitDataChange(ObjectType.STAFF, result, CrudOperation.CREATED)
}
},
update: {
onSuccess(result) {
emitDataChange(ObjectType.STAFF, result, CrudOperation.UPDATED)
},
}
}),
getStaff
}
}

View File

@ -1,4 +1,12 @@
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@server/trpc/trpc.router';
import type { AppRouter } from '@server/trpc/types';
import {
createTRPCReact,
type inferReactQueryProcedureOptions,
} from '@trpc/react-query';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
export type ReactQueryOptions = inferReactQueryProcedureOptions<AppRouter>;
export type RouterInputs = inferRouterInputs<AppRouter>;
export type RouterOutputs = inferRouterOutputs<AppRouter>;
export const api = createTRPCReact<AppRouter>();

View File

@ -1,13 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"target": "es2022",
"module": "es2022",
"allowJs": true,
"esModuleInterop": true,
"lib": [
"dom",
"esnext"
"es2022"
],
"jsx": "react-jsx",
"declaration": true,
@ -16,7 +16,9 @@
"outDir": "dist",
"moduleResolution": "node",
"incremental": true,
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
"strict": true,
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"useDefineForClassFields": false
},
"include": [
"src"

View File

@ -25,15 +25,15 @@ model Taxonomy {
}
model Term {
id String @id @default(cuid())
id String @id @default(cuid())
name String
posts Post[]
taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id])
taxonomyId String? @map("taxonomy_id")
order Float? @map("order")
posts Post[]
taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id])
taxonomyId String? @map("taxonomy_id")
order Float? @map("order")
description String?
parentId String? @map("parent_id")
parentId String? @map("parent_id")
parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade)
children Term[] @relation("ChildParent")
ancestors TermAncestry[] @relation("DescendantToAncestor")
@ -96,6 +96,7 @@ model Staff {
receivedMsgs Message[] @relation("message_receiver")
registerToken String?
ownedResources Resource[]
meta Json?
@@index([officerId])
@@index([deptId])
@ -185,15 +186,15 @@ model AppConfig {
model Post {
// 字符串类型字段
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
type String? // 帖子类型,可为空
state String? // 状态 未读、处理中、已回答
title String? // 帖子标题,可为空
content String? // 帖子内容,可为空
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
type String? // 帖子类型,可为空
state String? // 状态 未读、处理中、已回答
title String? // 帖子标题,可为空
content String? // 帖子内容,可为空
domainId String? @map("domain_id")
term Term? @relation(fields: [termId], references: [id])
termId String? @map("term_id")
term Term? @relation(fields: [termId], references: [id])
termId String? @map("term_id")
// 日期时间类型字段
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @map("updated_at")
@ -202,16 +203,16 @@ model Post {
authorId String? @map("author_id")
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
visits Visit[] // 访问记录,关联 Visit 模型
views Int @default(0)
likes Int @default(0)
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 模型
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
receivers Staff[] @relation("post_receiver")
parentId String? @map("parent_id")
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
resources Resource[] // 附件列表
isPublic Boolean? @default(true) @map("is_public")
meta Json? // 签名 和 IP 和 tags
meta Json? // 签名 和 IP 和 tags
// 复合索引
@@index([type, domainId]) // 类型和域组合查询
@ -247,8 +248,8 @@ model Visit {
views Int @default(1) @map("views")
// sourceIP String? @map("source_ip")
// 关联关系
visitorId String? @map("visitor_id")
visitor Staff? @relation(fields: [visitorId], references: [id])
visitorId String? @map("visitor_id")
visitor Staff? @relation(fields: [visitorId], references: [id])
postId String? @map("post_id")
post Post? @relation(fields: [postId], references: [id])
message Message? @relation(fields: [messageId], references: [id])

View File

@ -38,6 +38,12 @@ export type AppLocalSettings = {
export type StaffDto = Staff & {
domain?: Department;
department?: Department;
meta?: {
photoUrl?: string
office?: string
email?: string
rank?: string
}
};
export interface AuthDto {
token: string;