Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
73a257cba5
|
@ -38,7 +38,7 @@ export class BaseService<
|
||||||
protected prisma: PrismaClient,
|
protected prisma: PrismaClient,
|
||||||
protected objectType: string,
|
protected objectType: string,
|
||||||
protected enableOrder: boolean = false,
|
protected enableOrder: boolean = false,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the name of the model dynamically.
|
* Retrieves the name of the model dynamically.
|
||||||
|
@ -457,6 +457,7 @@ export class BaseService<
|
||||||
try {
|
try {
|
||||||
// 获取总记录数
|
// 获取总记录数
|
||||||
const total = (await this.getModel().count({ where })) as number;
|
const total = (await this.getModel().count({ where })) as number;
|
||||||
|
|
||||||
// 获取分页数据
|
// 获取分页数据
|
||||||
const items = (await this.getModel().findMany({
|
const items = (await this.getModel().findMany({
|
||||||
where,
|
where,
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class PostRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly postService: PostService,
|
private readonly postService: PostService,
|
||||||
) {}
|
) { }
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
.input(PostCreateArgsSchema)
|
.input(PostCreateArgsSchema)
|
||||||
|
@ -96,5 +96,15 @@ export class PostRouter {
|
||||||
const ip = getClientIp(req);
|
const ip = getClientIp(req);
|
||||||
return await this.postService.findManyWithCursor(input, staff, ip);
|
return await this.postService.findManyWithCursor(input, staff, ip);
|
||||||
}),
|
}),
|
||||||
|
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);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { z } from 'zod';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
|
@ -119,72 +120,14 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
async findManyWithPagination(args: { page?: number; pageSize?: number; where?: Prisma.StaffWhereInput; select?: Prisma.StaffSelect<DefaultArgs>; }) {
|
||||||
// * 根据关键词或ID集合查找员工
|
if (args.where.deptId && typeof args.where.deptId === 'string') {
|
||||||
// * @param data 包含关键词、域ID和ID集合的对象
|
const childDepts = await this.departmentService.getDescendantIds(args.where.deptId, true);
|
||||||
// * @returns 匹配的员工记录列表
|
args.where.deptId = {
|
||||||
// */
|
in: childDepts
|
||||||
// async findMany(data: z.infer<typeof StaffMethodSchema.findMany>) {
|
}
|
||||||
// const { keyword, domainId, ids, deptId, limit = 30 } = data;
|
}
|
||||||
// const idResults = ids
|
|
||||||
// ? await db.staff.findMany({
|
return super.findManyWithPagination(args)
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { RoleRouter } from '@server/models/rbac/role.router';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
|
|
||||||
logger = new Logger(TrpcRouter.name);
|
logger = new Logger(TrpcRouter.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
|
@ -30,8 +31,11 @@ export class TrpcRouter {
|
||||||
private readonly app_config: AppConfigRouter,
|
private readonly app_config: AppConfigRouter,
|
||||||
private readonly message: MessageRouter,
|
private readonly message: MessageRouter,
|
||||||
private readonly visitor: VisitRouter,
|
private readonly visitor: VisitRouter,
|
||||||
// private readonly websocketService: WebSocketService
|
|
||||||
) {}
|
) { }
|
||||||
|
getRouter() {
|
||||||
|
return
|
||||||
|
}
|
||||||
appRouter = this.trpc.router({
|
appRouter = this.trpc.router({
|
||||||
transform: this.transform.router,
|
transform: this.transform.router,
|
||||||
post: this.post.router,
|
post: this.post.router,
|
||||||
|
@ -66,4 +70,3 @@ export class TrpcRouter {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export type AppRouter = TrpcRouter[`appRouter`];
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { TrpcRouter } from "./trpc.router";
|
||||||
|
|
||||||
|
export type AppRouter = TrpcRouter[`appRouter`];
|
|
@ -10,7 +10,6 @@
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
// "skipLibCheck": true,
|
// "skipLibCheck": true,
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
|
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
|
||||||
import { Leader } from './types';
|
import { StaffDto } from "@nice/common";
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
interface LeaderCardProps {
|
export interface SendCardProps {
|
||||||
leader: Leader;
|
staff: StaffDto;
|
||||||
isSelected: boolean;
|
|
||||||
onSelect: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardProps) {
|
export function SendCard({ staff }: SendCardProps) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
@ -17,20 +16,36 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className={`
|
className={`
|
||||||
bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100
|
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">
|
<div className="flex flex-col sm:flex-row">
|
||||||
{/* Image Container */}
|
{/* Image Container */}
|
||||||
<div className="sm:w-48 h-64 sm:h-auto flex-shrink-0">
|
<div className="sm:w-48 h-64 sm:h-auto flex-shrink-0 bg-gray-100 flex items-center justify-center">
|
||||||
<img
|
{staff.meta?.photoUrl ? (
|
||||||
src={leader.imageUrl}
|
<img
|
||||||
alt={leader.name}
|
src={staff.meta.photoUrl}
|
||||||
className="w-full h-full object-cover"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
|
@ -39,13 +54,13 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-xl font-semibold text-gray-900">
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
{leader.name}
|
{staff.showname}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="px-3 py-1 text-sm font-medium bg-blue-50 text-primary rounded-full">
|
<span className="px-3 py-1 text-sm font-medium bg-blue-50 text-primary rounded-full">
|
||||||
{leader.rank}
|
{staff.meta?.rank || '未设置职级'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 mb-4">{leader.division}</p>
|
<p className="text-gray-600 mb-4">{staff.department?.name || '未设置部门'}</p>
|
||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<div className="space-y-2 text-sm text-gray-600">
|
<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"
|
<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" />
|
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>
|
</svg>
|
||||||
{leader.email}
|
{staff.meta?.email || '未设置邮箱'}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
<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" />
|
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>
|
</svg>
|
||||||
{leader.phone}
|
{staff.phoneNumber || '未设置电话'}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
<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" />
|
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>
|
</svg>
|
||||||
{leader.office}
|
{staff.meta?.office || '未设置办公室'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={onSelect}
|
type="primary"
|
||||||
className="mt-auto w-full sm:w-auto flex items-center justify-center gap-2
|
size='large'
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<PaperAirplaneIcon className="w-5 h-5" />
|
<PaperAirplaneIcon className="w-5 h-5" />
|
||||||
Compose Letter
|
发送信件
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,50 +1,95 @@
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Leader } from './types';
|
|
||||||
import { leaders } from './mock';
|
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import Filter from './filter';
|
import { SendCard } from './SendCard';
|
||||||
import LeaderCard from './LeaderCard';
|
import { Spin, Empty, Input, Alert, message, Pagination } from 'antd';
|
||||||
import { Spin, Empty } from 'antd';
|
import { api } from '@nice/client';
|
||||||
import { api } from 'packages/client/dist';
|
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() {
|
export default function WriteLetterPage() {
|
||||||
|
|
||||||
const [selectedLeader, setSelectedLeader] = useState<Leader | null>(null);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
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(() => {
|
const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({
|
||||||
return leaders.filter(leader => {
|
page: currentPage,
|
||||||
const matchesSearch = leader.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
pageSize,
|
||||||
leader.rank.toLowerCase().includes(searchQuery.toLowerCase());
|
where: {
|
||||||
const matchesDivision = selectedDivision === 'all' || leader.division === selectedDivision;
|
deptId: selectedDept,
|
||||||
return matchesSearch && matchesDivision;
|
OR: [{
|
||||||
});
|
showname: {
|
||||||
}, [searchQuery, selectedDivision]);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100">
|
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
<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>
|
<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
|
<motion.div
|
||||||
className="grid grid-cols-1 gap-6"
|
className="grid grid-cols-1 gap-6"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
{filteredLeaders.map((leader) => (
|
{data?.items.map((item) => (
|
||||||
<LeaderCard
|
<SendCard
|
||||||
key={leader.id}
|
key={item.id}
|
||||||
leader={leader}
|
staff={item as any}
|
||||||
isSelected={selectedLeader?.id === leader.id}
|
|
||||||
onSelect={() => setSelectedLeader(leader)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
@ -55,10 +100,27 @@ export default function WriteLetterPage() {
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
<Empty></Empty>
|
<Empty
|
||||||
|
description="没有找到匹配的收信人"
|
||||||
|
className="py-12"
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ interface DepartmentSelectProps {
|
||||||
domain?: boolean;
|
domain?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
size?: "small" | "middle" | "large";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DepartmentSelect({
|
export default function DepartmentSelect({
|
||||||
|
@ -24,6 +25,7 @@ export default function DepartmentSelect({
|
||||||
placeholder = "选择单位",
|
placeholder = "选择单位",
|
||||||
multiple = false,
|
multiple = false,
|
||||||
rootId = null,
|
rootId = null,
|
||||||
|
size,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
domain = undefined,
|
domain = undefined,
|
||||||
}: DepartmentSelectProps) {
|
}: DepartmentSelectProps) {
|
||||||
|
@ -150,6 +152,7 @@ export default function DepartmentSelect({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
|
size={size}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
className={className}
|
className={className}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
|
||||||
|
export default function LetterList(){
|
||||||
|
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ export function useDepartment() {
|
||||||
|
|
||||||
const update = api.department.update.useMutation({
|
const update = api.department.update.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
emitDataChange(ObjectType.DEPARTMENT, result as any, CrudOperation.UPDATED)
|
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) => {
|
const getDept = <T = DepartmentDto>(key: string) => {
|
||||||
return findQueryData<T>(queryClient, api.department, key);
|
return findQueryData<T>(queryClient, api.department, key);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 函数
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,18 +1,5 @@
|
||||||
import { api } from "../trpc";
|
import { useEntity } from "./useEntity";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getQueryKey } from "@trpc/react-query";
|
|
||||||
import { Prisma } from "packages/common/dist";
|
|
||||||
|
|
||||||
export function useMessage() {
|
export function useMessage() {
|
||||||
const queryClient = useQueryClient();
|
return useEntity("message")
|
||||||
const queryKey = getQueryKey(api.message);
|
}
|
||||||
const create:any = api.message.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
create
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,37 +1,4 @@
|
||||||
import { api } from "../trpc";
|
import { useEntity } from "./useEntity";
|
||||||
|
|
||||||
export function usePost() {
|
export function usePost() {
|
||||||
const utils = api.useUtils();
|
return useEntity("post")
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
|
@ -3,7 +3,15 @@ import { api } from "../trpc"; // Adjust path as necessary
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { CrudOperation, emitDataChange, EventBus } from "../../event";
|
import { CrudOperation, emitDataChange, EventBus } from "../../event";
|
||||||
import { ObjectType } from "@nice/common";
|
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 queryClient = useQueryClient();
|
||||||
const queryKey = getQueryKey(api.rolemap);
|
const queryKey = getQueryKey(api.rolemap);
|
||||||
|
|
||||||
|
@ -30,10 +38,10 @@ export function useRoleMap() {
|
||||||
});
|
});
|
||||||
const deleteMany = api.rolemap.deleteMany.useMutation({
|
const deleteMany = api.rolemap.deleteMany.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
emitDataChange(ObjectType.ROLE_MAP, result as any, CrudOperation.DELETED)
|
emitDataChange(ObjectType.ROLE_MAP, result as any, CrudOperation.DELETED)
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -45,3 +53,4 @@ export function useRoleMap() {
|
||||||
addRoleForObjects
|
addRoleForObjects
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,29 @@
|
||||||
import { getQueryKey } from "@trpc/react-query";
|
|
||||||
import { api } from "../trpc"; // Adjust path as necessary
|
import { api } from "../trpc"; // Adjust path as necessary
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { ObjectType, Staff } from "@nice/common";
|
import { ObjectType, Staff } from "@nice/common";
|
||||||
import { findQueryData } from "../utils";
|
import { findQueryData } from "../utils";
|
||||||
import { CrudOperation, emitDataChange } from "../../event";
|
import { CrudOperation, emitDataChange } from "../../event";
|
||||||
|
import { useEntity } from "./useEntity";
|
||||||
export function useStaff() {
|
export function useStaff() {
|
||||||
const queryClient = useQueryClient();
|
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 getStaff = (key: string) => {
|
const getStaff = (key: string) => {
|
||||||
return findQueryData<Staff>(queryClient, api.staff, key);
|
return findQueryData<Staff>(queryClient, api.staff, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create,
|
...useEntity("staff", {
|
||||||
update,
|
create: {
|
||||||
softDeleteByIds,
|
onSuccess(result) {
|
||||||
getStaff,
|
emitDataChange(ObjectType.STAFF, result, CrudOperation.CREATED)
|
||||||
updateUserDomain
|
}
|
||||||
};
|
},
|
||||||
|
update: {
|
||||||
|
onSuccess(result) {
|
||||||
|
emitDataChange(ObjectType.STAFF, result, CrudOperation.UPDATED)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getStaff
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { createTRPCReact } from '@trpc/react-query';
|
import type { AppRouter } from '@server/trpc/types';
|
||||||
import type { AppRouter } from '@server/trpc/trpc.router';
|
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>();
|
export const api = createTRPCReact<AppRouter>();
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "es2022",
|
||||||
"module": "esnext",
|
"module": "es2022",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"esnext"
|
"es2022"
|
||||||
],
|
],
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
@ -16,7 +16,9 @@
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
"strict": true,
|
||||||
|
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
||||||
|
"useDefineForClassFields": false
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|
|
@ -25,15 +25,15 @@ model Taxonomy {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Term {
|
model Term {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
posts Post[]
|
posts Post[]
|
||||||
taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id])
|
taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id])
|
||||||
taxonomyId String? @map("taxonomy_id")
|
taxonomyId String? @map("taxonomy_id")
|
||||||
order Float? @map("order")
|
order Float? @map("order")
|
||||||
description String?
|
description String?
|
||||||
parentId String? @map("parent_id")
|
parentId String? @map("parent_id")
|
||||||
|
|
||||||
parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade)
|
parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade)
|
||||||
children Term[] @relation("ChildParent")
|
children Term[] @relation("ChildParent")
|
||||||
ancestors TermAncestry[] @relation("DescendantToAncestor")
|
ancestors TermAncestry[] @relation("DescendantToAncestor")
|
||||||
|
@ -96,6 +96,7 @@ model Staff {
|
||||||
receivedMsgs Message[] @relation("message_receiver")
|
receivedMsgs Message[] @relation("message_receiver")
|
||||||
registerToken String?
|
registerToken String?
|
||||||
ownedResources Resource[]
|
ownedResources Resource[]
|
||||||
|
meta Json?
|
||||||
|
|
||||||
@@index([officerId])
|
@@index([officerId])
|
||||||
@@index([deptId])
|
@@index([deptId])
|
||||||
|
@ -185,15 +186,15 @@ model AppConfig {
|
||||||
|
|
||||||
model Post {
|
model Post {
|
||||||
// 字符串类型字段
|
// 字符串类型字段
|
||||||
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
|
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
|
||||||
type String? // 帖子类型,可为空
|
type String? // 帖子类型,可为空
|
||||||
state String? // 状态 : 未读、处理中、已回答
|
state String? // 状态 : 未读、处理中、已回答
|
||||||
title String? // 帖子标题,可为空
|
title String? // 帖子标题,可为空
|
||||||
content String? // 帖子内容,可为空
|
content String? // 帖子内容,可为空
|
||||||
|
|
||||||
domainId String? @map("domain_id")
|
domainId String? @map("domain_id")
|
||||||
term Term? @relation(fields: [termId], references: [id])
|
term Term? @relation(fields: [termId], references: [id])
|
||||||
termId String? @map("term_id")
|
termId String? @map("term_id")
|
||||||
// 日期时间类型字段
|
// 日期时间类型字段
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @map("updated_at")
|
updatedAt DateTime @map("updated_at")
|
||||||
|
@ -202,16 +203,16 @@ model Post {
|
||||||
authorId String? @map("author_id")
|
authorId String? @map("author_id")
|
||||||
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
||||||
visits Visit[] // 访问记录,关联 Visit 模型
|
visits Visit[] // 访问记录,关联 Visit 模型
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
likes Int @default(0)
|
likes Int @default(0)
|
||||||
|
|
||||||
receivers Staff[] @relation("post_receiver")
|
receivers Staff[] @relation("post_receiver")
|
||||||
parentId String? @map("parent_id")
|
parentId String? @map("parent_id")
|
||||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
||||||
resources Resource[] // 附件列表
|
resources Resource[] // 附件列表
|
||||||
isPublic Boolean? @default(true) @map("is_public")
|
isPublic Boolean? @default(true) @map("is_public")
|
||||||
meta Json? // 签名 和 IP 和 tags
|
meta Json? // 签名 和 IP 和 tags
|
||||||
|
|
||||||
// 复合索引
|
// 复合索引
|
||||||
@@index([type, domainId]) // 类型和域组合查询
|
@@index([type, domainId]) // 类型和域组合查询
|
||||||
|
@ -247,8 +248,8 @@ model Visit {
|
||||||
views Int @default(1) @map("views")
|
views Int @default(1) @map("views")
|
||||||
// sourceIP String? @map("source_ip")
|
// sourceIP String? @map("source_ip")
|
||||||
// 关联关系
|
// 关联关系
|
||||||
visitorId String? @map("visitor_id")
|
visitorId String? @map("visitor_id")
|
||||||
visitor Staff? @relation(fields: [visitorId], references: [id])
|
visitor Staff? @relation(fields: [visitorId], references: [id])
|
||||||
postId String? @map("post_id")
|
postId String? @map("post_id")
|
||||||
post Post? @relation(fields: [postId], references: [id])
|
post Post? @relation(fields: [postId], references: [id])
|
||||||
message Message? @relation(fields: [messageId], references: [id])
|
message Message? @relation(fields: [messageId], references: [id])
|
||||||
|
|
|
@ -38,6 +38,12 @@ export type AppLocalSettings = {
|
||||||
export type StaffDto = Staff & {
|
export type StaffDto = Staff & {
|
||||||
domain?: Department;
|
domain?: Department;
|
||||||
department?: Department;
|
department?: Department;
|
||||||
|
meta?: {
|
||||||
|
photoUrl?: string
|
||||||
|
office?: string
|
||||||
|
email?: string
|
||||||
|
rank?: string
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export interface AuthDto {
|
export interface AuthDto {
|
||||||
token: string;
|
token: string;
|
||||||
|
|
Loading…
Reference in New Issue