1. import { NextRequest, NextResponse } from "next/server";
2. import { prisma } from "@/lib/prisma";
3. import { verifyAuthToken } from "@/lib/auth";
4.
5. function getAuthUser(req: NextRequest) {
6.   const token = req.cookies.get("auth_token")?.value;
7.   if (!token) return null;
8.   return verifyAuthToken(token);
9. }
10.
11. export async function GET(req: NextRequest) {
12.   const auth = getAuthUser(req);
13.   if (!auth) {
14.     return NextResponse.json({ message: "未登录" }, { status: 401 });
15.   }
16.
17.   const { searchParams } = new URL(req.url);
18.   const status = searchParams.get("status");
19.   const keyword = searchParams.get("keyword")?.trim() ?? "";
20.   const departmentIdRaw = searchParams.get("departmentId");
21.   const pageRaw = searchParams.get("page");
22.   const pageSizeRaw = searchParams.get("pageSize");
23.
24.   const page = Number.isInteger(Number(pageRaw)) && Number(pageRaw) > 0 ? Number(pageRaw) : 1;
25.   const requestedPageSize =
26.     Number.isInteger(Number(pageSizeRaw)) && Number(pageSizeRaw) > 0 ? Number(pageSizeRaw) : 10;
27.   const pageSize = Math.min(requestedPageSize, 100);
28.
29.   const where: any = {};
30.   const now = new Date();
31.
32.   if (
33.     status === "IN_PROGRESS" ||
34.     status === "UNDER_REVIEW" ||
35.     status === "COMPLETED" ||
36.     status === "REVISION"
37.   ) {
38.     where.status = status;
39.   }
40.
41.   if (status === "OVERDUE") {
42.     where.AND = where.AND || [];
43.     if (!Array.isArray(where.AND)) where.AND = [where.AND];
44.     (where.AND as any[]).push({ dueDate: { lt: now } }, { status: { not: "COMPLETED" } });
45.   }
46.
47.   if (keyword) {
48.     where.title = {
49.       contains: keyword,
50.       mode: "insensitive",
51.     };
52.   }
53.
54.   if (departmentIdRaw) {
55.     const departmentId = Number(departmentIdRaw);
56.     if (Number.isInteger(departmentId) && departmentId > 0) {
57.       where.departments = {
58.         some: {
59.           departmentId,
60.         },
61.       };
62.     }
63.   }
64.
65.   // 收集所有 responsibleId 和 memberId
66.   const responsibleIds = searchParams.getAll("responsibleId").map(Number).filter((v) => Number.isInteger(v) && v > 0);
67.   const memberIds = searchParams.getAll("memberId").map(Number).filter((v) => Number.isInteger(v) && v > 0);
68.
69.   if (responsibleIds.length > 0 && memberIds.length > 0) {
70.     // 同时有负责人和成员过滤 → OR 逻辑（只要满足其一即可）
71.     const orConditions: any[] = [];
72.     if (responsibleIds.length > 0) {
73.       orConditions.push({ responsible: { some: { id: { in: responsibleIds } } } });
74.     }
75.     if (memberIds.length > 0) {
76.       orConditions.push({ members: { some: { userId: { in: memberIds } } } });
77.     }
78.     // 合并到已有的 AND 条件中
79.     if (where.AND) {
80.       where.AND = [...(Array.isArray(where.AND) ? where.AND : [where.AND]), { OR: orConditions }];
81.     } else {
82.       where.AND = [{ OR: orConditions }];
83.     }
84.   } else if (responsibleIds.length > 0) {
85.     // 只有负责人过滤
86.     where.responsible = { some: { id: { in: responsibleIds } } };
87.   } else if (memberIds.length > 0) {
88.     // 只有成员过滤
89.     where.members = { some: { userId: { in: memberIds } } };
90.   }
91.
92.   const skip = (page - 1) * pageSize;
93.
94.   const [
95.     tasks,
96.     total,
97.     totalAll,
98.     totalInProgress,
99.     totalUnderReview,
100.     totalRevision,
101.     totalCompleted,
102.     totalOverdue,
103.     deptRows,
104.   ] = await prisma.$transaction([
105.     prisma.task.findMany({
106.       where,
107.       skip,
108.       take: pageSize,
109.       orderBy: { createdAt: "desc" },
110.       include: {
111.         departments: {
112.           include: { department: true },
113.         },
114.         responsible: true,
115.         members: {
116.           include: { user: true },
117.         },
118.       },
119.     }),
120.     prisma.task.count({ where }),
121.     prisma.task.count(),
122.     prisma.task.count({ where: { status: "IN_PROGRESS" } }),
123.     prisma.task.count({ where: { status: "UNDER_REVIEW" } }),
124.     prisma.task.count({ where: { status: "REVISION" } }),
125.     prisma.task.count({ where: { status: "COMPLETED" } }),
126.     prisma.task.count({
127.       where: {
128.         AND: [{ dueDate: { lt: now } }, { status: { not: "COMPLETED" } }],
129.       },
130.     }),
131.     prisma.taskDepartment.findMany({
132.       select: {
133.         departmentId: true,
134.       },
135.     }),
136.   ]);
137.
138.   const totalPages = total === 0 ? 0 : Math.ceil(total / pageSize);
139.   const deptCounts = deptRows.reduce<Record<number, number>>((acc, row) => {
140.     acc[row.departmentId] = (acc[row.departmentId] ?? 0) + 1;
141.     return acc;
142.   }, {});
143.
144.   return NextResponse.json({
145.     tasks,
146.     pagination: {
147.       page,
148.       pageSize,
149.       total,
150.       totalPages,
151.     },
152.     stats: {
153.       ALL: totalAll,
154.       IN_PROGRESS: totalInProgress,
155.       UNDER_REVIEW: totalUnderReview,
156.       REVISION: totalRevision,
157.       COMPLETED: totalCompleted,
158.       OVERDUE: totalOverdue,
159.     },
160.     deptCounts,
161.   });
162. }
163.
164. export async function POST(req: NextRequest) {
165.   const auth = getAuthUser(req);
166.   if (!auth) {
167.     return NextResponse.json({ message: "未登录" }, { status: 401 });
168.   }
169.
170.   const body = await req.json();
171.   const {
172.     title,
173.     description,
174.     departmentIds,
175.     dueDate,
176.     responsibleIds,
177.     memberIds,
178.   } = body;
179.
180.   if (!title || !description) {
181.     return NextResponse.json(
182.       { message: "标题和内容必填" },
183.       { status: 400 }
184.     );
185.   }
186.
187.   if (!Array.isArray(responsibleIds) || responsibleIds.length === 0) {
188.     return NextResponse.json(
189.       { message: "请选择至少一名负责人" },
190.       { status: 400 }
191.     );
192.   }
193.
194.   if (!Array.isArray(memberIds) || memberIds.length === 0) {
195.     return NextResponse.json(
196.       { message: "请选择至少一名成员" },
197.       { status: 400 }
198.     );
199.   }
200.
201.   const deptIdsRaw: unknown = departmentIds;
202.   const deptIds: number[] = Array.isArray(deptIdsRaw)
203.     ? (deptIdsRaw as any[])
204.         .map((v) => Number(v))
205.         .filter((v) => Number.isInteger(v))
206.     : [];
207.
208.   if (deptIds.length === 0) {
209.     if (auth.departmentId) {
210.       deptIds.push(auth.departmentId);
211.     } else {
212.       return NextResponse.json(
213.         { message: "请至少选择一个所属大队" },
214.         { status: 400 }
215.       );
216.     }
217.   }
218.
219.   const task = await prisma.task.create({
220.     data: {

diff --git a/home/longshao/renwuguanli_zhian/src/app/api/tasks/route.ts b/home/longshao/renwuguanli_zhian/src/app/api/tasks/route.ts
index 0000000..0000000 100644
--- a/home/longshao/renwuguanli_zhian/src/app/api/tasks/route.ts
+++ b/home/longshao/renwuguanli_zhian/src/app/api/tasks/route.ts
@@ -1,220 +1,220 @@
 import { NextRequest, NextResponse } from "next/server";
 import { prisma } from "@/lib/prisma";
 import { verifyAuthToken } from "@/lib/auth";

 function getAuthUser(req: NextRequest) {
   const token = req.cookies.get("auth_token")?.value;
   if (!token) return null;
   return verifyAuthToken(token);
 }

 export async function GET(req: NextRequest) {
   const auth = getAuthUser(req);
   if (!auth) {
     return NextResponse.json({ message: "未登录" }, { status: 401 });
   }

   const { searchParams } = new URL(req.url);
   const status = searchParams.get("status");
   const keyword = searchParams.get("keyword")?.trim() ?? "";
   const departmentIdRaw = searchParams.get("departmentId");
   const pageRaw = searchParams.get("page");
   const pageSizeRaw = searchParams.get("pageSize");

   const page = Number.isInteger(Number(pageRaw)) && Number(pageRaw) > 0 ? Number(pageRaw) : 1;
   const requestedPageSize =
     Number.isInteger(Number(pageSizeRaw)) && Number(pageSizeRaw) > 0 ? Number(pageSizeRaw) : 10;
   const pageSize = Math.min(requestedPageSize, 100);

   const where: any = {};
   const now = new Date();

   if (
     status === "IN_PROGRESS" ||
     status === "UNDER_REVIEW" ||
     status === "COMPLETED" ||
     status === "REVISION"
   ) {
     where.status = status;
   }

   if (status === "OVERDUE") {
     where.AND = where.AND || [];
     if (!Array.isArray(where.AND)) where.AND = [where.AND];
     (where.AND as any[]).push({ dueDate: { lt: now } }, { status: { not: "COMPLETED" } });
   }

   if (keyword) {
     where.title = {
       contains: keyword,
       mode: "insensitive",
     };
   }

   if (departmentIdRaw) {
     const departmentId = Number(departmentIdRaw);
     if (Number.isInteger(departmentId) && departmentId > 0) {
       where.departments = {
         some: {
           departmentId,
         },
       };
     }
   }

   // 收集所有 responsibleId 和 memberId
   const responsibleIds = searchParams.getAll("responsibleId").map(Number).filter((v) => Number.isInteger(v) && v > 0);
   const memberIds = searchParams.getAll("memberId").map(Number).filter((v) => Number.isInteger(v) && v > 0);

   if (responsibleIds.length > 0 && memberIds.length > 0) {
     // 同时有负责人和成员过滤 → OR 逻辑（只要满足其一即可）
     const orConditions: any[] = [];
     if (responsibleIds.length > 0) {
       orConditions.push({ responsible: { some: { id: { in: responsibleIds } } } });
     }
     if (memberIds.length > 0) {
       orConditions.push({ members: { some: { userId: { in: memberIds } } } });
     }
     // 合并到已有的 AND 条件中
     if (where.AND) {
       where.AND = [...(Array.isArray(where.AND) ? where.AND : [where.AND]), { OR: orConditions }];
     } else {
       where.AND = [{ OR: orConditions }];
     }
   } else if (responsibleIds.length > 0) {
     // 只有负责人过滤
     where.responsible = { some: { id: { in: responsibleIds } } };
   } else if (memberIds.length > 0) {
     // 只有成员过滤
     where.members = { some: { userId: { in: memberIds } } };
   }

   const skip = (page - 1) * pageSize;

   const [
     tasks,
     total,
     totalAll,
     totalInProgress,
     totalUnderReview,
     totalRevision,
     totalCompleted,
     totalOverdue,
     deptRows,
   ] = await prisma.$transaction([
     prisma.task.findMany({
       where,
       skip,
       take: pageSize,
       orderBy: { createdAt: "desc" },
       include: {
         departments: {
           include: { department: true },
         },
         responsible: true,
         members: {
           include: { user: true },
         },
       },
     }),
     prisma.task.count({ where }),
     prisma.task.count(),
     prisma.task.count({ where: { status: "IN_PROGRESS" } }),
     prisma.task.count({ where: { status: "UNDER_REVIEW" } }),
     prisma.task.count({ where: { status: "REVISION" } }),
     prisma.task.count({ where: { status: "COMPLETED" } }),
     prisma.task.count({
       where: {
         AND: [{ dueDate: { lt: now } }, { status: { not: "COMPLETED" } }],
       },
     }),
     prisma.taskDepartment.findMany({
       select: {
         departmentId: true,
       },
     }),
   ]);

   const totalPages = total === 0 ? 0 : Math.ceil(total / pageSize);
   const deptCounts = deptRows.reduce<Record<number, number>>((acc, row) => {
     acc[row.departmentId] = (acc[row.departmentId] ?? 0) + 1;
     return acc;
   }, {});

   return NextResponse.json({
     tasks,
     pagination: {
       page,
       pageSize,
       total,
       totalPages,
     },
     stats: {
       ALL: totalAll,
       IN_PROGRESS: totalInProgress,
       UNDER_REVIEW: totalUnderReview,
       REVISION: totalRevision,
       COMPLETED: totalCompleted,
       OVERDUE: totalOverdue,
     },
     deptCounts,
   });
 }

 export async function POST(req: NextRequest) {
   const auth = getAuthUser(req);
   if (!auth) {
     return NextResponse.json({ message: "未登录" }, { status: 401 });
   }

   const body = await req.json();
   const {
     title,
     description,
     departmentIds,
     dueDate,
     responsibleIds,
     memberIds,
   } = body;

   if (!title || !description) {
     return NextResponse.json(
       { message: "标题和内容必填" },
       { status: 400 }
     );
   }

   if (!Array.isArray(responsibleIds) || responsibleIds.length === 0) {
     return NextResponse.json(
       { message: "请选择至少一名负责人" },
       { status: 400 }
     );
   }

   if (!Array.isArray(memberIds) || memberIds.length === 0) {
     return NextResponse.json(
       { message: "请选择至少一名成员" },
       { status: 400 }
     );
   }

   const deptIdsRaw: unknown = departmentIds;
   const deptIds: number[] = Array.isArray(deptIdsRaw)
     ? (deptIdsRaw as any[])
         .map((v) => Number(v))
         .filter((v) => Number.isInteger(v))
     : [];

   if (deptIds.length === 0) {
     if (auth.departmentId) {
       deptIds.push(auth.departmentId);
     } else {
       return NextResponse.json(
         { message: "请至少选择一个所属大队" },
         { status: 400 }
       );
     }
   }

   const task = await prisma.task.create({
     data: {

path view_range forceReadLargeFiles command


240.     REVISION: number;
241.     COMPLETED: number;
242.     OVERDUE: number;
243.   }>({
244.     ALL: 0,
245.     IN_PROGRESS: 0,
246.     UNDER_REVIEW: 0,
247.     REVISION: 0,
248.     COMPLETED: 0,
249.     OVERDUE: 0,
250.   });
251.   const [currentUser, setCurrentUser] = useState<{
252.     id: number;
253.     role: string;
254.     departmentId: number | null;
255.   } | null>(null);
256.   const [loading, setLoading] = useState(false);
257.   const [error, setError] = useState<string | null>(null);
258.   const [users, setUsers] = useState<UserOption[]>([]);
259.   const [usersLoaded, setUsersLoaded] = useState(false);
260.   const [usersError, setUsersError] = useState<string | null>(null);
261.   const [showCreate, setShowCreate] = useState(false);
262.   const [departments, setDepartments] = useState<{"id": number; "name": string}[]>([]);
263.   const [activeDeptId, setActiveDeptId] = useState<number | null>(null);
264.   const [deptCounts, setDeptCounts] = useState<Record<number, number>>({});
265.
266.   const [newDepartmentIds, setNewDepartmentIds] = useState<number[]>([]);
267.   const [newTitle, setNewTitle] = useState("");
268.   const [newDescription, setNewDescription] = useState("");
269.   const [newDueDate, setNewDueDate] = useState("");
270.   const [newResponsibleIds, setNewResponsibleIds] = useState<number[]>([]);
271.   const [newMemberIds, setNewMemberIds] = useState<number[]>([]);
272.   const [createLoading, setCreateLoading] = useState(false);
273.   const [createError, setCreateError] = useState<string | null>(null);
274.   const router = useRouter();
275.   const [showExport, setShowExport] = useState(false);
276.   const [showImport, setShowImport] = useState(false);
277.   const [searchKeyword, setSearchKeyword] = useState("");
278.   const [pageSize, setPageSize] = useState<number>(10);
279.   const [currentPage, setCurrentPage] = useState(1);
280.   const fileInputRef = useRef<HTMLInputElement>(null);
281.   const importMenuRef = useRef<HTMLDivElement | null>(null);
282.   const exportMenuRef = useRef<HTMLDivElement | null>(null);
283.
284.   // 人员过滤状态
285.   const [filterRole, setFilterRole] = useState<"ALL" | "RESPONSIBLE" | "MEMBER">("ALL");
286.   const [filterUserIds, setFilterUserIds] = useState<number[]>([]);
287.   const [filterUserSelectOpen, setFilterUserSelectOpen] = useState(false);
288.   const filterSelectRef = useRef<HTMLDivElement | null>(null);
289.
290.   function isOverdue(task: Task) {
291.     if (!task.dueDate) return false;
292.     const due = new Date(task.dueDate);
293.     return due.getTime() < Date.now() && task.status !== "COMPLETED";
294.   }
295.
296.   async function loadTasks() {
297.     setLoading(true);
298.     setError(null);
299.     try {
300.       const params = new URLSearchParams();
301.       params.set("page", String(currentPage));
302.       params.set("pageSize", String(pageSize));
303.       if (active !== "ALL") params.set("status", active);
304.       const keyword = searchKeyword.trim();
305.       if (keyword) params.set("keyword", keyword);
306.       if (activeDeptId !== null) params.set("departmentId", String(activeDeptId));
307.
308.       // 人员过滤参数
309.       if (filterUserIds.length > 0) {
310.         if (filterRole === "RESPONSIBLE") {
311.           // 只按负责人过滤
312.           filterUserIds.forEach((id) => params.append("responsibleId", String(id)));
313.         } else if (filterRole === "MEMBER") {
314.           // 只按成员过滤
315.           filterUserIds.forEach((id) => params.append("memberId", String(id)));
316.         } else {
317.           // 全部：同时传 responsibleId 和 memberId（OR 逻辑在前端处理，这里用多个请求合并太复杂，
318.           // 改为后端支持多值，或者简单地取第一个用户同时查两个角色）
319.           // 简化方案：单用户时两个都传（OR），多用户时取第一个
320.           if (filterUserIds.length === 1) {
321.             params.append("responsibleId", String(filterUserIds[0]));
322.             params.append("memberId", String(filterUserIds[0]));
323.           } else {
324.             // 多用户：传所有ID作为 responsibleId 和 memberId
325.             filterUserIds.forEach((id) => {
326.               params.append("responsibleId", String(id));
327.               params.append("memberId", String(id));
328.             });
329.           }
330.         }
331.       }
332.
333.       const res = await fetch("/api/tasks?" + params.toString());
334.       if (!res.ok) {
335.         const data = await res.json().catch(() => ({}));
336.         setError(data.message || "加载失败");
337.         setTasks([]);
338.         setTotalTasks(0);
339.         setTotalPages(0);
340.         return;
341.       }
342.
343.       const data = await res.json();
344.       const list: Task[] = (data.tasks || []).map((t: any) => ({
345.         id: t.id,
346.         title: t.title,
347.         status: t.status,
348.         dueDate: t.dueDate,
349.         departmentNames: (t.departments || [])
350.           .map((d: any) => d.department?.name)
351.           .filter((n: string | null | undefined) => !!n)
352.           .join(", "),
353.         departmentIds: (t.departments || []).map((d: any) => d.departmentId),
354.         responsibleNames: (t.responsible || []).map((u: any) => u.name).join(", "),
355.         memberNames: (t.members || []).map((m: any) => m.user?.name).join(", "),
356.         responsibleIds: (t.responsible || []).map((u: any) => u.id),
357.       }));
358.
359.       const pagination = data.pagination ?? {};
360.       setTasks(list);
361.       setTotalTasks(Number(pagination.total) || 0);
362.       setTotalPages(Number(pagination.totalPages) || 0);
363.
364.       if (data.stats) {
365.         setStats({
366.           ALL: Number(data.stats.ALL) || 0,
367.           IN_PROGRESS: Number(data.stats.IN_PROGRESS) || 0,
368.           UNDER_REVIEW: Number(data.stats.UNDER_REVIEW) || 0,
369.           REVISION: Number(data.stats.REVISION) || 0,
370.           COMPLETED: Number(data.stats.COMPLETED) || 0,
371.           OVERDUE: Number(data.stats.OVERDUE) || 0,
372.         });
373.       }
374.
375.       setDeptCounts(data.deptCounts || {});
376.     } catch {
377.       setError("网络错误");
378.       setTasks([]);
379.       setTotalTasks(0);
380.       setTotalPages(0);
381.     } finally {
382.       setLoading(false);
383.     }
384.   }
385.
386.   useEffect(() => {
387.     loadTasks();
388.   }, [active, searchKeyword, activeDeptId, pageSize, currentPage]);
389.
390.   // 人员过滤变化时重新加载
391.   useEffect(() => {
392.     loadTasks();
393.   }, [filterRole, filterUserIds]);
394.
395.   useEffect(() => {
396.     function handleMouseDown(ev: MouseEvent) {
397.       const target = ev.target;
398.       if (!(target instanceof Node)) return;
399.
400.       if (showImport && importMenuRef.current && !importMenuRef.current.contains(target)) {
401.         setShowImport(false);
402.       }
403.
404.       if (showExport && exportMenuRef.current && !exportMenuRef.current.contains(target)) {
405.         setShowExport(false);
406.       }
407.
408.       if (filterUserSelectOpen && filterSelectRef.current && !filterSelectRef.current.contains(target)) {
409.         setFilterUserSelectOpen(false);
410.       }
411.     }
412.
413.     function handleKeyDown(ev: KeyboardEvent) {
414.       if (ev.key === "Escape") {
415.         setShowImport(false);
416.         setShowExport(false);
417.       }
418.     }
419.
420.     document.addEventListener("mousedown", handleMouseDown);
421.     document.addEventListener("keydown", handleKeyDown);
422.     return () => {
423.       document.removeEventListener("mousedown", handleMouseDown);
424.       document.removeEventListener("keydown", handleKeyDown);
425.     };
426.   }, [showImport, showExport]);
427.
428.   useEffect(() => {
429.     async function loadMe() {
430.       try {
431.         const res = await fetch("/api/auth/me");
432.         if (!res.ok) return;
433.         const data = await res.json();
434.         setCurrentUser(data.user);
435.         // 默认带入当前用户作为过滤条件
436.         if (data.user?.id) {
437.           setFilterUserIds([data.user.id]);
438.         }
439.       } catch {
440.         // ignore
441.       }
442.     }
443.     loadMe();
444.   }, []);
445.
446.   useEffect(() => {
447.     async function loadUsers() {
448.       setUsersError(null);
449.       try {
450.         const res = await fetch("/api/users");
451.         if (!res.ok) {
452.           const data = await res.json().catch(() => ({}));
453.           setUsersError(data.message || "加载用户失败");
454.           return;
455.         }
456.         const data = await res.json();
457.         const list = (data.users || []).map((u: any) => ({
458.           id: u.id,
459.           name: u.name,
460.           badgeNo: u.badgeNo,
461.           departmentId: u.department?.id ?? null,
462.           departmentName: u.department?.name ?? null,
463.         }));
464.         setUsers(list);
465.         setUsersLoaded(true);
466.       } catch {
467.         setUsersError("网络错误");
468.       }
469.     }
470.     loadUsers();

diff --git a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
index 0000000..0000000 100644
--- a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
+++ b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
@@ -240,231 +240,231 @@
     REVISION: number;
     COMPLETED: number;
     OVERDUE: number;
   }>({
     ALL: 0,
     IN_PROGRESS: 0,
     UNDER_REVIEW: 0,
     REVISION: 0,
     COMPLETED: 0,
     OVERDUE: 0,
   });
   const [currentUser, setCurrentUser] = useState<{
     id: number;
     role: string;
     departmentId: number | null;
   } | null>(null);
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [users, setUsers] = useState<UserOption[]>([]);
   const [usersLoaded, setUsersLoaded] = useState(false);
   const [usersError, setUsersError] = useState<string | null>(null);
   const [showCreate, setShowCreate] = useState(false);
   const [departments, setDepartments] = useState<{"id": number; "name": string}[]>([]);
   const [activeDeptId, setActiveDeptId] = useState<number | null>(null);
   const [deptCounts, setDeptCounts] = useState<Record<number, number>>({});

   const [newDepartmentIds, setNewDepartmentIds] = useState<number[]>([]);
   const [newTitle, setNewTitle] = useState("");
   const [newDescription, setNewDescription] = useState("");
   const [newDueDate, setNewDueDate] = useState("");
   const [newResponsibleIds, setNewResponsibleIds] = useState<number[]>([]);
   const [newMemberIds, setNewMemberIds] = useState<number[]>([]);
   const [createLoading, setCreateLoading] = useState(false);
   const [createError, setCreateError] = useState<string | null>(null);
   const router = useRouter();
   const [showExport, setShowExport] = useState(false);
   const [showImport, setShowImport] = useState(false);
   const [searchKeyword, setSearchKeyword] = useState("");
   const [pageSize, setPageSize] = useState<number>(10);
   const [currentPage, setCurrentPage] = useState(1);
   const fileInputRef = useRef<HTMLInputElement>(null);
   const importMenuRef = useRef<HTMLDivElement | null>(null);
   const exportMenuRef = useRef<HTMLDivElement | null>(null);

   // 人员过滤状态
   const [filterRole, setFilterRole] = useState<"ALL" | "RESPONSIBLE" | "MEMBER">("ALL");
   const [filterUserIds, setFilterUserIds] = useState<number[]>([]);
   const [filterUserSelectOpen, setFilterUserSelectOpen] = useState(false);
   const filterSelectRef = useRef<HTMLDivElement | null>(null);

   function isOverdue(task: Task) {
     if (!task.dueDate) return false;
     const due = new Date(task.dueDate);
     return due.getTime() < Date.now() && task.status !== "COMPLETED";
   }

   async function loadTasks() {
     setLoading(true);
     setError(null);
     try {
       const params = new URLSearchParams();
       params.set("page", String(currentPage));
       params.set("pageSize", String(pageSize));
       if (active !== "ALL") params.set("status", active);
       const keyword = searchKeyword.trim();
       if (keyword) params.set("keyword", keyword);
       if (activeDeptId !== null) params.set("departmentId", String(activeDeptId));

       // 人员过滤参数
       if (filterUserIds.length > 0) {
         if (filterRole === "RESPONSIBLE") {
           // 只按负责人过滤
           filterUserIds.forEach((id) => params.append("responsibleId", String(id)));
         } else if (filterRole === "MEMBER") {
           // 只按成员过滤
           filterUserIds.forEach((id) => params.append("memberId", String(id)));
         } else {
           // 全部：同时传 responsibleId 和 memberId（OR 逻辑在前端处理，这里用多个请求合并太复杂，
           // 改为后端支持多值，或者简单地取第一个用户同时查两个角色）
           // 简化方案：单用户时两个都传（OR），多用户时取第一个
           if (filterUserIds.length === 1) {
             params.append("responsibleId", String(filterUserIds[0]));
             params.append("memberId", String(filterUserIds[0]));
           } else {
             // 多用户：传所有ID作为 responsibleId 和 memberId
             filterUserIds.forEach((id) => {
               params.append("responsibleId", String(id));
               params.append("memberId", String(id));
             });
           }
         }
       }

       const res = await fetch("/api/tasks?" + params.toString());
       if (!res.ok) {
         const data = await res.json().catch(() => ({}));
         setError(data.message || "加载失败");
         setTasks([]);
         setTotalTasks(0);
         setTotalPages(0);
         return;
       }

       const data = await res.json();
       const list: Task[] = (data.tasks || []).map((t: any) => ({
         id: t.id,
         title: t.title,
         status: t.status,
         dueDate: t.dueDate,
         departmentNames: (t.departments || [])
           .map((d: any) => d.department?.name)
           .filter((n: string | null | undefined) => !!n)
           .join(", "),
         departmentIds: (t.departments || []).map((d: any) => d.departmentId),
         responsibleNames: (t.responsible || []).map((u: any) => u.name).join(", "),
         memberNames: (t.members || []).map((m: any) => m.user?.name).join(", "),
         responsibleIds: (t.responsible || []).map((u: any) => u.id),
       }));

       const pagination = data.pagination ?? {};
       setTasks(list);
       setTotalTasks(Number(pagination.total) || 0);
       setTotalPages(Number(pagination.totalPages) || 0);

       if (data.stats) {
         setStats({
           ALL: Number(data.stats.ALL) || 0,
           IN_PROGRESS: Number(data.stats.IN_PROGRESS) || 0,
           UNDER_REVIEW: Number(data.stats.UNDER_REVIEW) || 0,
           REVISION: Number(data.stats.REVISION) || 0,
           COMPLETED: Number(data.stats.COMPLETED) || 0,
           OVERDUE: Number(data.stats.OVERDUE) || 0,
         });
       }

       setDeptCounts(data.deptCounts || {});
     } catch {
       setError("网络错误");
       setTasks([]);
       setTotalTasks(0);
       setTotalPages(0);
     } finally {
       setLoading(false);
     }
   }

   useEffect(() => {
     loadTasks();
   }, [active, searchKeyword, activeDeptId, pageSize, currentPage]);

   // 人员过滤变化时重新加载
   useEffect(() => {
     loadTasks();
   }, [filterRole, filterUserIds]);

   useEffect(() => {
     function handleMouseDown(ev: MouseEvent) {
       const target = ev.target;
       if (!(target instanceof Node)) return;

       if (showImport && importMenuRef.current && !importMenuRef.current.contains(target)) {
         setShowImport(false);
       }

       if (showExport && exportMenuRef.current && !exportMenuRef.current.contains(target)) {
         setShowExport(false);
       }

       if (filterUserSelectOpen && filterSelectRef.current && !filterSelectRef.current.contains(target)) {
         setFilterUserSelectOpen(false);
       }
     }

     function handleKeyDown(ev: KeyboardEvent) {
       if (ev.key === "Escape") {
         setShowImport(false);
         setShowExport(false);
       }
     }

     document.addEventListener("mousedown", handleMouseDown);
     document.addEventListener("keydown", handleKeyDown);
     return () => {
       document.removeEventListener("mousedown", handleMouseDown);
       document.removeEventListener("keydown", handleKeyDown);
     };
   }, [showImport, showExport]);

   useEffect(() => {
     async function loadMe() {
       try {
         const res = await fetch("/api/auth/me");
         if (!res.ok) return;
         const data = await res.json();
         setCurrentUser(data.user);
         // 默认带入当前用户作为过滤条件
         if (data.user?.id) {
           setFilterUserIds([data.user.id]);
         }
       } catch {
         // ignore
       }
     }
     loadMe();
   }, []);

   useEffect(() => {
     async function loadUsers() {
       setUsersError(null);
       try {
         const res = await fetch("/api/users");
         if (!res.ok) {
           const data = await res.json().catch(() => ({}));
           setUsersError(data.message || "加载用户失败");
           return;
         }
         const data = await res.json();
         const list = (data.users || []).map((u: any) => ({
           id: u.id,
           name: u.name,
           badgeNo: u.badgeNo,
           departmentId: u.department?.id ?? null,
           departmentName: u.department?.name ?? null,
         }));
         setUsers(list);
         setUsersLoaded(true);
       } catch {
         setUsersError("网络错误");
       }
     }
     loadUsers();


240.     REVISION: number;
241.     COMPLETED: number;
242.     OVERDUE: number;
243.   }>({
244.     ALL: 0,
245.     IN_PROGRESS: 0,
246.     UNDER_REVIEW: 0,
247.     REVISION: 0,
248.     COMPLETED: 0,
249.     OVERDUE: 0,
250.   });
251.   const [currentUser, setCurrentUser] = useState<{
252.     id: number;
253.     role: string;
254.     departmentId: number | null;
255.   } | null>(null);
256.   const [loading, setLoading] = useState(false);
257.   const [error, setError] = useState<string | null>(null);
258.   const [users, setUsers] = useState<UserOption[]>([]);
259.   const [usersLoaded, setUsersLoaded] = useState(false);
260.   const [usersError, setUsersError] = useState<string | null>(null);
261.   const [showCreate, setShowCreate] = useState(false);
262.   const [departments, setDepartments] = useState<{"id": number; "name": string}[]>([]);
263.   const [activeDeptId, setActiveDeptId] = useState<number | null>(null);
264.   const [deptCounts, setDeptCounts] = useState<Record<number, number>>({});
265.
266.   const [newDepartmentIds, setNewDepartmentIds] = useState<number[]>([]);
267.   const [newTitle, setNewTitle] = useState("");
268.   const [newDescription, setNewDescription] = useState("");
269.   const [newDueDate, setNewDueDate] = useState("");
270.   const [newResponsibleIds, setNewResponsibleIds] = useState<number[]>([]);
271.   const [newMemberIds, setNewMemberIds] = useState<number[]>([]);
272.   const [createLoading, setCreateLoading] = useState(false);
273.   const [createError, setCreateError] = useState<string | null>(null);
274.   const router = useRouter();
275.   const [showExport, setShowExport] = useState(false);
276.   const [showImport, setShowImport] = useState(false);
277.   const [searchKeyword, setSearchKeyword] = useState("");
278.   const [pageSize, setPageSize] = useState<number>(10);
279.   const [currentPage, setCurrentPage] = useState(1);
280.   const fileInputRef = useRef<HTMLInputElement>(null);
281.   const importMenuRef = useRef<HTMLDivElement | null>(null);
282.   const exportMenuRef = useRef<HTMLDivElement | null>(null);
283.
284.   // 人员过滤状态
285.   const [filterRole, setFilterRole] = useState<"ALL" | "RESPONSIBLE" | "MEMBER">("ALL");
286.   const [filterUserIds, setFilterUserIds] = useState<number[]>([]);
287.   const [filterUserSelectOpen, setFilterUserSelectOpen] = useState(false);
288.   const filterSelectRef = useRef<HTMLDivElement | null>(null);
289.
290.   function isOverdue(task: Task) {
291.     if (!task.dueDate) return false;
292.     const due = new Date(task.dueDate);
293.     return due.getTime() < Date.now() && task.status !== "COMPLETED";
294.   }
295.
296.   async function loadTasks() {
297.     setLoading(true);
298.     setError(null);
299.     try {
300.       const params = new URLSearchParams();
301.       params.set("page", String(currentPage));
302.       params.set("pageSize", String(pageSize));
303.       if (active !== "ALL") params.set("status", active);
304.       const keyword = searchKeyword.trim();
305.       if (keyword) params.set("keyword", keyword);
306.       if (activeDeptId !== null) params.set("departmentId", String(activeDeptId));
307.
308.       // 人员过滤参数
309.       if (filterUserIds.length > 0) {
310.         if (filterRole === "RESPONSIBLE") {
311.           // 只按负责人过滤
312.           filterUserIds.forEach((id) => params.append("responsibleId", String(id)));
313.         } else if (filterRole === "MEMBER") {
314.           // 只按成员过滤
315.           filterUserIds.forEach((id) => params.append("memberId", String(id)));
316.         } else {
317.           // 全部：同时传 responsibleId 和 memberId（OR 逻辑在前端处理，这里用多个请求合并太复杂，
318.           // 改为后端支持多值，或者简单地取第一个用户同时查两个角色）
319.           // 简化方案：单用户时两个都传（OR），多用户时取第一个
320.           if (filterUserIds.length === 1) {
321.             params.append("responsibleId", String(filterUserIds[0]));
322.             params.append("memberId", String(filterUserIds[0]));
323.           } else {
324.             // 多用户：传所有ID作为 responsibleId 和 memberId
325.             filterUserIds.forEach((id) => {
326.               params.append("responsibleId", String(id));
327.               params.append("memberId", String(id));
328.             });
329.           }
330.         }
331.       }
332.
333.       const res = await fetch("/api/tasks?" + params.toString());
334.       if (!res.ok) {
335.         const data = await res.json().catch(() => ({}));
336.         setError(data.message || "加载失败");
337.         setTasks([]);
338.         setTotalTasks(0);
339.         setTotalPages(0);
340.         return;
341.       }
342.
343.       const data = await res.json();
344.       const list: Task[] = (data.tasks || []).map((t: any) => ({
345.         id: t.id,
346.         title: t.title,
347.         status: t.status,
348.         dueDate: t.dueDate,
349.         departmentNames: (t.departments || [])
350.           .map((d: any) => d.department?.name)
351.           .filter((n: string | null | undefined) => !!n)
352.           .join(", "),
353.         departmentIds: (t.departments || []).map((d: any) => d.departmentId),
354.         responsibleNames: (t.responsible || []).map((u: any) => u.name).join(", "),
355.         memberNames: (t.members || []).map((m: any) => m.user?.name).join(", "),
356.         responsibleIds: (t.responsible || []).map((u: any) => u.id),
357.       }));
358.
359.       const pagination = data.pagination ?? {};
360.       setTasks(list);
361.       setTotalTasks(Number(pagination.total) || 0);
362.       setTotalPages(Number(pagination.totalPages) || 0);
363.
364.       if (data.stats) {
365.         setStats({
366.           ALL: Number(data.stats.ALL) || 0,
367.           IN_PROGRESS: Number(data.stats.IN_PROGRESS) || 0,
368.           UNDER_REVIEW: Number(data.stats.UNDER_REVIEW) || 0,
369.           REVISION: Number(data.stats.REVISION) || 0,
370.           COMPLETED: Number(data.stats.COMPLETED) || 0,
371.           OVERDUE: Number(data.stats.OVERDUE) || 0,
372.         });
373.       }
374.
375.       setDeptCounts(data.deptCounts || {});
376.     } catch {
377.       setError("网络错误");
378.       setTasks([]);
379.       setTotalTasks(0);
380.       setTotalPages(0);
381.     } finally {
382.       setLoading(false);
383.     }
384.   }
385.
386.   useEffect(() => {
387.     loadTasks();
388.   }, [active, searchKeyword, activeDeptId, pageSize, currentPage]);
389.
390.   // 人员过滤变化时重新加载
391.   useEffect(() => {
392.     loadTasks();
393.   }, [filterRole, filterUserIds]);
394.
395.   useEffect(() => {
396.     function handleMouseDown(ev: MouseEvent) {
397.       const target = ev.target;
398.       if (!(target instanceof Node)) return;
399.
400.       if (showImport && importMenuRef.current && !importMenuRef.current.contains(target)) {
401.         setShowImport(false);
402.       }
403.
404.       if (showExport && exportMenuRef.current && !exportMenuRef.current.contains(target)) {
405.         setShowExport(false);
406.       }
407.
408.       if (filterUserSelectOpen && filterSelectRef.current && !filterSelectRef.current.contains(target)) {
409.         setFilterUserSelectOpen(false);
410.       }
411.     }
412.
413.     function handleKeyDown(ev: KeyboardEvent) {
414.       if (ev.key === "Escape") {
415.         setShowImport(false);
416.         setShowExport(false);
417.       }
418.     }
419.
420.     document.addEventListener("mousedown", handleMouseDown);
421.     document.addEventListener("keydown", handleKeyDown);
422.     return () => {
423.       document.removeEventListener("mousedown", handleMouseDown);
424.       document.removeEventListener("keydown", handleKeyDown);
425.     };
426.   }, [showImport, showExport]);
427.
428.   useEffect(() => {
429.     async function loadMe() {
430.       try {
431.         const res = await fetch("/api/auth/me");
432.         if (!res.ok) return;
433.         const data = await res.json();
434.         setCurrentUser(data.user);
435.         // 默认带入当前用户作为过滤条件
436.         if (data.user?.id) {
437.           setFilterUserIds([data.user.id]);
438.         }
439.       } catch {
440.         // ignore
441.       }
442.     }
443.     loadMe();
444.   }, []);
445.
446.   useEffect(() => {
447.     async function loadUsers() {
448.       setUsersError(null);
449.       try {
450.         const res = await fetch("/api/users");
451.         if (!res.ok) {
452.           const data = await res.json().catch(() => ({}));
453.           setUsersError(data.message || "加载用户失败");
454.           return;
455.         }
456.         const data = await res.json();
457.         const list = (data.users || []).map((u: any) => ({
458.           id: u.id,
459.           name: u.name,
460.           badgeNo: u.badgeNo,
461.           departmentId: u.department?.id ?? null,
462.           departmentName: u.department?.name ?? null,
463.         }));
464.         setUsers(list);
465.         setUsersLoaded(true);
466.       } catch {
467.         setUsersError("网络错误");
468.       }
469.     }
470.     loadUsers();

diff --git a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
index 0000000..0000000 100644
--- a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
+++ b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
@@ -240,231 +240,231 @@
     REVISION: number;
     COMPLETED: number;
     OVERDUE: number;
   }>({
     ALL: 0,
     IN_PROGRESS: 0,
     UNDER_REVIEW: 0,
     REVISION: 0,
     COMPLETED: 0,
     OVERDUE: 0,
   });
   const [currentUser, setCurrentUser] = useState<{
     id: number;
     role: string;
     departmentId: number | null;
   } | null>(null);
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [users, setUsers] = useState<UserOption[]>([]);
   const [usersLoaded, setUsersLoaded] = useState(false);
   const [usersError, setUsersError] = useState<string | null>(null);
   const [showCreate, setShowCreate] = useState(false);
   const [departments, setDepartments] = useState<{"id": number; "name": string}[]>([]);
   const [activeDeptId, setActiveDeptId] = useState<number | null>(null);
   const [deptCounts, setDeptCounts] = useState<Record<number, number>>({});

   const [newDepartmentIds, setNewDepartmentIds] = useState<number[]>([]);
   const [newTitle, setNewTitle] = useState("");
   const [newDescription, setNewDescription] = useState("");
   const [newDueDate, setNewDueDate] = useState("");
   const [newResponsibleIds, setNewResponsibleIds] = useState<number[]>([]);
   const [newMemberIds, setNewMemberIds] = useState<number[]>([]);
   const [createLoading, setCreateLoading] = useState(false);
   const [createError, setCreateError] = useState<string | null>(null);
   const router = useRouter();
   const [showExport, setShowExport] = useState(false);
   const [showImport, setShowImport] = useState(false);
   const [searchKeyword, setSearchKeyword] = useState("");
   const [pageSize, setPageSize] = useState<number>(10);
   const [currentPage, setCurrentPage] = useState(1);
   const fileInputRef = useRef<HTMLInputElement>(null);
   const importMenuRef = useRef<HTMLDivElement | null>(null);
   const exportMenuRef = useRef<HTMLDivElement | null>(null);

   // 人员过滤状态
   const [filterRole, setFilterRole] = useState<"ALL" | "RESPONSIBLE" | "MEMBER">("ALL");
   const [filterUserIds, setFilterUserIds] = useState<number[]>([]);
   const [filterUserSelectOpen, setFilterUserSelectOpen] = useState(false);
   const filterSelectRef = useRef<HTMLDivElement | null>(null);

   function isOverdue(task: Task) {
     if (!task.dueDate) return false;
     const due = new Date(task.dueDate);
     return due.getTime() < Date.now() && task.status !== "COMPLETED";
   }

   async function loadTasks() {
     setLoading(true);
     setError(null);
     try {
       const params = new URLSearchParams();
       params.set("page", String(currentPage));
       params.set("pageSize", String(pageSize));
       if (active !== "ALL") params.set("status", active);
       const keyword = searchKeyword.trim();
       if (keyword) params.set("keyword", keyword);
       if (activeDeptId !== null) params.set("departmentId", String(activeDeptId));

       // 人员过滤参数
       if (filterUserIds.length > 0) {
         if (filterRole === "RESPONSIBLE") {
           // 只按负责人过滤
           filterUserIds.forEach((id) => params.append("responsibleId", String(id)));
         } else if (filterRole === "MEMBER") {
           // 只按成员过滤
           filterUserIds.forEach((id) => params.append("memberId", String(id)));
         } else {
           // 全部：同时传 responsibleId 和 memberId（OR 逻辑在前端处理，这里用多个请求合并太复杂，
           // 改为后端支持多值，或者简单地取第一个用户同时查两个角色）
           // 简化方案：单用户时两个都传（OR），多用户时取第一个
           if (filterUserIds.length === 1) {
             params.append("responsibleId", String(filterUserIds[0]));
             params.append("memberId", String(filterUserIds[0]));
           } else {
             // 多用户：传所有ID作为 responsibleId 和 memberId
             filterUserIds.forEach((id) => {
               params.append("responsibleId", String(id));
               params.append("memberId", String(id));
             });
           }
         }
       }

       const res = await fetch("/api/tasks?" + params.toString());
       if (!res.ok) {
         const data = await res.json().catch(() => ({}));
         setError(data.message || "加载失败");
         setTasks([]);
         setTotalTasks(0);
         setTotalPages(0);
         return;
       }

       const data = await res.json();
       const list: Task[] = (data.tasks || []).map((t: any) => ({
         id: t.id,
         title: t.title,
         status: t.status,
         dueDate: t.dueDate,
         departmentNames: (t.departments || [])
           .map((d: any) => d.department?.name)
           .filter((n: string | null | undefined) => !!n)
           .join(", "),
         departmentIds: (t.departments || []).map((d: any) => d.departmentId),
         responsibleNames: (t.responsible || []).map((u: any) => u.name).join(", "),
         memberNames: (t.members || []).map((m: any) => m.user?.name).join(", "),
         responsibleIds: (t.responsible || []).map((u: any) => u.id),
       }));

       const pagination = data.pagination ?? {};
       setTasks(list);
       setTotalTasks(Number(pagination.total) || 0);
       setTotalPages(Number(pagination.totalPages) || 0);

       if (data.stats) {
         setStats({
           ALL: Number(data.stats.ALL) || 0,
           IN_PROGRESS: Number(data.stats.IN_PROGRESS) || 0,
           UNDER_REVIEW: Number(data.stats.UNDER_REVIEW) || 0,
           REVISION: Number(data.stats.REVISION) || 0,
           COMPLETED: Number(data.stats.COMPLETED) || 0,
           OVERDUE: Number(data.stats.OVERDUE) || 0,
         });
       }

       setDeptCounts(data.deptCounts || {});
     } catch {
       setError("网络错误");
       setTasks([]);
       setTotalTasks(0);
       setTotalPages(0);
     } finally {
       setLoading(false);
     }
   }

   useEffect(() => {
     loadTasks();
   }, [active, searchKeyword, activeDeptId, pageSize, currentPage]);

   // 人员过滤变化时重新加载
   useEffect(() => {
     loadTasks();
   }, [filterRole, filterUserIds]);

   useEffect(() => {
     function handleMouseDown(ev: MouseEvent) {
       const target = ev.target;
       if (!(target instanceof Node)) return;

       if (showImport && importMenuRef.current && !importMenuRef.current.contains(target)) {
         setShowImport(false);
       }

       if (showExport && exportMenuRef.current && !exportMenuRef.current.contains(target)) {
         setShowExport(false);
       }

       if (filterUserSelectOpen && filterSelectRef.current && !filterSelectRef.current.contains(target)) {
         setFilterUserSelectOpen(false);
       }
     }

     function handleKeyDown(ev: KeyboardEvent) {
       if (ev.key === "Escape") {
         setShowImport(false);
         setShowExport(false);
       }
     }

     document.addEventListener("mousedown", handleMouseDown);
     document.addEventListener("keydown", handleKeyDown);
     return () => {
       document.removeEventListener("mousedown", handleMouseDown);
       document.removeEventListener("keydown", handleKeyDown);
     };
   }, [showImport, showExport]);

   useEffect(() => {
     async function loadMe() {
       try {
         const res = await fetch("/api/auth/me");
         if (!res.ok) return;
         const data = await res.json();
         setCurrentUser(data.user);
         // 默认带入当前用户作为过滤条件
         if (data.user?.id) {
           setFilterUserIds([data.user.id]);
         }
       } catch {
         // ignore
       }
     }
     loadMe();
   }, []);

   useEffect(() => {
     async function loadUsers() {
       setUsersError(null);
       try {
         const res = await fetch("/api/users");
         if (!res.ok) {
           const data = await res.json().catch(() => ({}));
           setUsersError(data.message || "加载用户失败");
           return;
         }
         const data = await res.json();
         const list = (data.users || []).map((u: any) => ({
           id: u.id,
           name: u.name,
           badgeNo: u.badgeNo,
           departmentId: u.department?.id ?? null,
           departmentName: u.department?.name ?? null,
         }));
         setUsers(list);
         setUsersLoaded(true);
       } catch {
         setUsersError("网络错误");
       }
     }
     loadUsers();


960.             >
961.               <button
962.                 type="button"
963.                 onClick={() => {
964.                   setShowExport((prev) => !prev);
965.                   setShowImport(false);
966.                 }}
967.                 className="px-3 py-1.5 rounded-md border border-slate-300 bg-white text-xs font-medium text-slate-700 hover:bg-slate-50"
968.               >
969.                 导出
970.               </button>
971.               {showExport && (
972.

973.                   <button
974.                     type="button"
975.                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
976.                     onClick={() => {
977.                       const params =
978.                         active === "ALL"
979.                           ? "?format=xlsx"
980.                           : `?format=xlsx&status=${encodeURIComponent(active)}`;
981.                       window.open(`/api/tasks/export${params}`, "_blank");
982.                       setShowExport(false);
983.                     }}
984.                   >
985.                     Excel
986.                   </button>
987.                   <button
988.                     type="button"
989.                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
990.                     onClick={() => {
991.                       const params =
992.                         active === "ALL"
993.                           ? "?format=csv"
994.                           : `?format=csv&status=${encodeURIComponent(active)}`;
995.                       window.open(`/api/tasks/export${params}`, "_blank");
996.                       setShowExport(false);
997.                     }}
998.                   >
999.                     CSV
1000.                   </button>
1001.

1002.               )}
1003.

1004.

1005.

1006.
1007.

1008.

1009.              <input
1010.               type="text"
1011.               placeholder="输入任务名称搜索..."
1012.               value={searchKeyword}
1013.               onChange={(e) => {
1014.                 setSearchKeyword(e.target.value);
1015.                 setCurrentPage(1);
1016.               }}
1017.               className="w-full rounded-md border border-slate-300 px-3 py-1.5 text-xs text-slate-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
1018.             />
1019.

1020.           <button
1021.             onClick={() => setShowCreate((v) => !v)}
1022.             className="px-4 py-1.5 rounded-md bg-blue-600 text-xs font-bold text-white hover:bg-blue-700 shadow-sm whitespace-nowrap"
1023.           >
1024.             + 新增
1025.           </button>
1026.

1027.

1028.
1029.       {/* 人员筛选栏 */}
1030.

1031.         按人员筛选：
1032.         <select
1033.           value={filterRole}
1034.           onChange={(e) => {
1035.             setFilterRole(e.target.value as "ALL" | "RESPONSIBLE" | "MEMBER");
1036.             setCurrentPage(1);
1037.           }}
1038.           className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 focus:outline-none focus:border-blue-500"
1039.         >
1040.           <option value="ALL">全部角色</option>
1041.           <option value="RESPONSIBLE">负责人</option>
1042.           <option value="MEMBER">成员</option>
1043.         </select>
1044.
1045.

1046.           {filterUserIds.map((uid) => {
1047.             const u = users.find((x) => x.id === uid);
1048.             const label = u ? `${u.name}` : `用户#${uid}`;
1049.             return (
1050.
1054.                 {label}
1055.                 <button
1056.                   type="button"
1057.                   onClick={() => {
1058.                     setFilterUserIds((prev) => prev.filter((id) => id !== uid));
1059.                     setCurrentPage(1);
1060.                   }}
1061.                   className="ml-0.5 text-blue-400 hover:text-blue-700 font-bold"
1062.                 >
1063.                   ✕
1064.                 </button>
1065.
1066.             );
1067.           })}
1068.

1069.
1070.

1071.           <button
1072.             type="button"
1073.             onClick={() => setFilterUserSelectOpen((v) => !v)}
1074.             className="text-blue-600 hover:text-blue-800 font-medium"
1075.           >
1076.             + 添加人员
1077.           </button>
1078.           {filterUserSelectOpen && (
1079.

1080.

1081.                 <input
1082.                   type="text"
1083.                   placeholder="输入姓名或警号搜索"
1084.                   className="w-full rounded border border-slate-200 px-2 py-1 text-xs focus:outline-none focus:border-blue-500"
1085.                   onChange={(e) => {
1086.                     const keyword = e.target.value.toLowerCase();
1087.                     const container = filterSelectRef.current?.querySelector('[data-user-list]');
1088.                     if (!container) return;
1089.                     container.querySelectorAll('[data-user-item]').forEach((el) => {
1090.                       const text = (el as HTMLElement).dataset.userItem?.toLowerCase() || "";
1091.                       (el as HTMLElement).style.display = text.includes(keyword) ? "" : "none";
1092.                     });
1093.                   }}
1094.                 />
1095.

1096.

1097.                 {users
1098.                   .filter((u) => !filterUserIds.includes(u.id))
1099.                   .map((u) => (
1100.                     <button
1101.                       key={u.id}
1102.                       type="button"
1103.                       data-user-item={`${u.name} ${u.badgeNo}`}
1104.                       className="w-full text-left px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100 rounded"
1105.                       onClick={() => {
1106.                         setFilterUserIds((prev) => [...prev, u.id]);
1107.                         setFilterUserSelectOpen(false);
1108.                         setCurrentPage(1);
1109.                       }}
1110.                     >
1111.                       {u.name}（{u.badgeNo}）
1112.                     </button>
1113.                   ))}
1114.                 {users.filter((u) => !filterUserIds.includes(u.id)).length === 0 && (
1115.
无可用人员

1116.                 )}
1117.

1118.

1119.           )}
1120.

1121.
1122.         {filterUserIds.length > 0 && (
1123.           <button
1124.             type="button"
1125.             onClick={() => {
1126.               setFilterUserIds([]);
1127.               setFilterRole("ALL");
1128.               setCurrentPage(1);
1129.             }}
1130.             className="ml-auto text-slate-400 hover:text-slate-600"
1131.           >
1132.             清除筛选
1133.           </button>
1134.         )}
1135.

diff --git a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
index 0000000..0000000 100644
--- a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
+++ b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
@@ -960,176 +960,176 @@
             >
               <button
                 type="button"
                 onClick={() => {
                   setShowExport((prev) => !prev);
                   setShowImport(false);
                 }}
                 className="px-3 py-1.5 rounded-md border border-slate-300 bg-white text-xs font-medium text-slate-700 hover:bg-slate-50"
               >
                 导出
               </button>
               {showExport && (

                   <button
                     type="button"
                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
                     onClick={() => {
                       const params =
                         active === "ALL"
                           ? "?format=xlsx"
                           : `?format=xlsx&status=${encodeURIComponent(active)}`;
                       window.open(`/api/tasks/export${params}`, "_blank");
                       setShowExport(false);
                     }}
                   >
                     Excel
                   </button>
                   <button
                     type="button"
                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
                     onClick={() => {
                       const params =
                         active === "ALL"
                           ? "?format=csv"
                           : `?format=csv&status=${encodeURIComponent(active)}`;
                       window.open(`/api/tasks/export${params}`, "_blank");
                       setShowExport(false);
                     }}
                   >
                     CSV
                   </button>

               )}

              <input
               type="text"
               placeholder="输入任务名称搜索..."
               value={searchKeyword}
               onChange={(e) => {
                 setSearchKeyword(e.target.value);
                 setCurrentPage(1);
               }}
               className="w-full rounded-md border border-slate-300 px-3 py-1.5 text-xs text-slate-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
             />

           <button
             onClick={() => setShowCreate((v) => !v)}
             className="px-4 py-1.5 rounded-md bg-blue-600 text-xs font-bold text-white hover:bg-blue-700 shadow-sm whitespace-nowrap"
           >
             + 新增
           </button>

       {/* 人员筛选栏 */}

         按人员筛选：
         <select
           value={filterRole}
           onChange={(e) => {
             setFilterRole(e.target.value as "ALL" | "RESPONSIBLE" | "MEMBER");
             setCurrentPage(1);
           }}
           className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 focus:outline-none focus:border-blue-500"
         >
           <option value="ALL">全部角色</option>
           <option value="RESPONSIBLE">负责人</option>
           <option value="MEMBER">成员</option>
         </select>

           {filterUserIds.map((uid) => {
             const u = users.find((x) => x.id === uid);
             const label = u ? `${u.name}` : `用户#${uid}`;
             return (

                 {label}
                 <button
                   type="button"
                   onClick={() => {
                     setFilterUserIds((prev) => prev.filter((id) => id !== uid));
                     setCurrentPage(1);
                   }}
                   className="ml-0.5 text-blue-400 hover:text-blue-700 font-bold"
                 >
                   ✕
                 </button>

             );
           })}

           <button
             type="button"
             onClick={() => setFilterUserSelectOpen((v) => !v)}
             className="text-blue-600 hover:text-blue-800 font-medium"
           >
             + 添加人员
           </button>
           {filterUserSelectOpen && (

                 <input
                   type="text"
                   placeholder="输入姓名或警号搜索"
                   className="w-full rounded border border-slate-200 px-2 py-1 text-xs focus:outline-none focus:border-blue-500"
                   onChange={(e) => {
                     const keyword = e.target.value.toLowerCase();
                     const container = filterSelectRef.current?.querySelector('[data-user-list]');
                     if (!container) return;
                     container.querySelectorAll('[data-user-item]').forEach((el) => {
                       const text = (el as HTMLElement).dataset.userItem?.toLowerCase() || "";
                       (el as HTMLElement).style.display = text.includes(keyword) ? "" : "none";
                     });
                   }}
                 />

                 {users
                   .filter((u) => !filterUserIds.includes(u.id))
                   .map((u) => (
                     <button
                       key={u.id}
                       type="button"
                       data-user-item={`${u.name} ${u.badgeNo}`}
                       className="w-full text-left px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100 rounded"
                       onClick={() => {
                         setFilterUserIds((prev) => [...prev, u.id]);
                         setFilterUserSelectOpen(false);
                         setCurrentPage(1);
                       }}
                     >
                       {u.name}（{u.badgeNo}）
                     </button>
                   ))}
                 {users.filter((u) => !filterUserIds.includes(u.id)).length === 0 && (

无可用人员

                 )}

           )}

         {filterUserIds.length > 0 && (
           <button
             type="button"
             onClick={() => {
               setFilterUserIds([]);
               setFilterRole("ALL");
               setCurrentPage(1);
             }}
             className="ml-auto text-slate-400 hover:text-slate-600"
           >
             清除筛选
           </button>
         )}


960.             >
961.               <button
962.                 type="button"
963.                 onClick={() => {
964.                   setShowExport((prev) => !prev);
965.                   setShowImport(false);
966.                 }}
967.                 className="px-3 py-1.5 rounded-md border border-slate-300 bg-white text-xs font-medium text-slate-700 hover:bg-slate-50"
968.               >
969.                 导出
970.               </button>
971.               {showExport && (
972.

973.                   <button
974.                     type="button"
975.                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
976.                     onClick={() => {
977.                       const params =
978.                         active === "ALL"
979.                           ? "?format=xlsx"
980.                           : `?format=xlsx&status=${encodeURIComponent(active)}`;
981.                       window.open(`/api/tasks/export${params}`, "_blank");
982.                       setShowExport(false);
983.                     }}
984.                   >
985.                     Excel
986.                   </button>
987.                   <button
988.                     type="button"
989.                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
990.                     onClick={() => {
991.                       const params =
992.                         active === "ALL"
993.                           ? "?format=csv"
994.                           : `?format=csv&status=${encodeURIComponent(active)}`;
995.                       window.open(`/api/tasks/export${params}`, "_blank");
996.                       setShowExport(false);
997.                     }}
998.                   >
999.                     CSV
1000.                   </button>
1001.

1002.               )}
1003.

1004.

1005.

1006.
1007.

1008.

1009.              <input
1010.               type="text"
1011.               placeholder="输入任务名称搜索..."
1012.               value={searchKeyword}
1013.               onChange={(e) => {
1014.                 setSearchKeyword(e.target.value);
1015.                 setCurrentPage(1);
1016.               }}
1017.               className="w-full rounded-md border border-slate-300 px-3 py-1.5 text-xs text-slate-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
1018.             />
1019.

1020.           <button
1021.             onClick={() => setShowCreate((v) => !v)}
1022.             className="px-4 py-1.5 rounded-md bg-blue-600 text-xs font-bold text-white hover:bg-blue-700 shadow-sm whitespace-nowrap"
1023.           >
1024.             + 新增
1025.           </button>
1026.

1027.

1028.
1029.       {/* 人员筛选栏 */}
1030.

1031.         按人员筛选：
1032.         <select
1033.           value={filterRole}
1034.           onChange={(e) => {
1035.             setFilterRole(e.target.value as "ALL" | "RESPONSIBLE" | "MEMBER");
1036.             setCurrentPage(1);
1037.           }}
1038.           className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 focus:outline-none focus:border-blue-500"
1039.         >
1040.           <option value="ALL">全部角色</option>
1041.           <option value="RESPONSIBLE">负责人</option>
1042.           <option value="MEMBER">成员</option>
1043.         </select>
1044.
1045.

1046.           {filterUserIds.map((uid) => {
1047.             const u = users.find((x) => x.id === uid);
1048.             const label = u ? `${u.name}` : `用户#${uid}`;
1049.             return (
1050.
1054.                 {label}
1055.                 <button
1056.                   type="button"
1057.                   onClick={() => {
1058.                     setFilterUserIds((prev) => prev.filter((id) => id !== uid));
1059.                     setCurrentPage(1);
1060.                   }}
1061.                   className="ml-0.5 text-blue-400 hover:text-blue-700 font-bold"
1062.                 >
1063.                   ✕
1064.                 </button>
1065.
1066.             );
1067.           })}
1068.

1069.
1070.

1071.           <button
1072.             type="button"
1073.             onClick={() => setFilterUserSelectOpen((v) => !v)}
1074.             className="text-blue-600 hover:text-blue-800 font-medium"
1075.           >
1076.             + 添加人员
1077.           </button>
1078.           {filterUserSelectOpen && (
1079.

1080.

1081.                 <input
1082.                   type="text"
1083.                   placeholder="输入姓名或警号搜索"
1084.                   className="w-full rounded border border-slate-200 px-2 py-1 text-xs focus:outline-none focus:border-blue-500"
1085.                   onChange={(e) => {
1086.                     const keyword = e.target.value.toLowerCase();
1087.                     const container = filterSelectRef.current?.querySelector('[data-user-list]');
1088.                     if (!container) return;
1089.                     container.querySelectorAll('[data-user-item]').forEach((el) => {
1090.                       const text = (el as HTMLElement).dataset.userItem?.toLowerCase() || "";
1091.                       (el as HTMLElement).style.display = text.includes(keyword) ? "" : "none";
1092.                     });
1093.                   }}
1094.                 />
1095.

1096.

1097.                 {users
1098.                   .filter((u) => !filterUserIds.includes(u.id))
1099.                   .map((u) => (
1100.                     <button
1101.                       key={u.id}
1102.                       type="button"
1103.                       data-user-item={`${u.name} ${u.badgeNo}`}
1104.                       className="w-full text-left px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100 rounded"
1105.                       onClick={() => {
1106.                         setFilterUserIds((prev) => [...prev, u.id]);
1107.                         setFilterUserSelectOpen(false);
1108.                         setCurrentPage(1);
1109.                       }}
1110.                     >
1111.                       {u.name}（{u.badgeNo}）
1112.                     </button>
1113.                   ))}
1114.                 {users.filter((u) => !filterUserIds.includes(u.id)).length === 0 && (
1115.
无可用人员

1116.                 )}
1117.

1118.

1119.           )}
1120.

1121.
1122.         {filterUserIds.length > 0 && (
1123.           <button
1124.             type="button"
1125.             onClick={() => {
1126.               setFilterUserIds([]);
1127.               setFilterRole("ALL");
1128.               setCurrentPage(1);
1129.             }}
1130.             className="ml-auto text-slate-400 hover:text-slate-600"
1131.           >
1132.             清除筛选
1133.           </button>
1134.         )}
1135.

diff --git a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
index 0000000..0000000 100644
--- a/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
+++ b/home/longshao/renwuguanli_zhian/src/app/(dashboard)/tasks/page.tsx
@@ -960,176 +960,176 @@
             >
               <button
                 type="button"
                 onClick={() => {
                   setShowExport((prev) => !prev);
                   setShowImport(false);
                 }}
                 className="px-3 py-1.5 rounded-md border border-slate-300 bg-white text-xs font-medium text-slate-700 hover:bg-slate-50"
               >
                 导出
               </button>
               {showExport && (

                   <button
                     type="button"
                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
                     onClick={() => {
                       const params =
                         active === "ALL"
                           ? "?format=xlsx"
                           : `?format=xlsx&status=${encodeURIComponent(active)}`;
                       window.open(`/api/tasks/export${params}`, "_blank");
                       setShowExport(false);
                     }}
                   >
                     Excel
                   </button>
                   <button
                     type="button"
                     className="w-full text-left px-3 py-1.5 hover:bg-slate-100"
                     onClick={() => {
                       const params =
                         active === "ALL"
                           ? "?format=csv"
                           : `?format=csv&status=${encodeURIComponent(active)}`;
                       window.open(`/api/tasks/export${params}`, "_blank");
                       setShowExport(false);
                     }}
                   >
                     CSV
                   </button>

               )}

              <input
               type="text"
               placeholder="输入任务名称搜索..."
               value={searchKeyword}
               onChange={(e) => {
                 setSearchKeyword(e.target.value);
                 setCurrentPage(1);
               }}
               className="w-full rounded-md border border-slate-300 px-3 py-1.5 text-xs text-slate-800 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
             />

           <button
             onClick={() => setShowCreate((v) => !v)}
             className="px-4 py-1.5 rounded-md bg-blue-600 text-xs font-bold text-white hover:bg-blue-700 shadow-sm whitespace-nowrap"
           >
             + 新增
           </button>

       {/* 人员筛选栏 */}

         按人员筛选：
         <select
           value={filterRole}
           onChange={(e) => {
             setFilterRole(e.target.value as "ALL" | "RESPONSIBLE" | "MEMBER");
             setCurrentPage(1);
           }}
           className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 focus:outline-none focus:border-blue-500"
         >
           <option value="ALL">全部角色</option>
           <option value="RESPONSIBLE">负责人</option>
           <option value="MEMBER">成员</option>
         </select>

           {filterUserIds.map((uid) => {
             const u = users.find((x) => x.id === uid);
             const label = u ? `${u.name}` : `用户#${uid}`;
             return (

                 {label}
                 <button
                   type="button"
                   onClick={() => {
                     setFilterUserIds((prev) => prev.filter((id) => id !== uid));
                     setCurrentPage(1);
                   }}
                   className="ml-0.5 text-blue-400 hover:text-blue-700 font-bold"
                 >
                   ✕
                 </button>

             );
           })}

           <button
             type="button"
             onClick={() => setFilterUserSelectOpen((v) => !v)}
             className="text-blue-600 hover:text-blue-800 font-medium"
           >
             + 添加人员
           </button>
           {filterUserSelectOpen && (

                 <input
                   type="text"
                   placeholder="输入姓名或警号搜索"
                   className="w-full rounded border border-slate-200 px-2 py-1 text-xs focus:outline-none focus:border-blue-500"
                   onChange={(e) => {
                     const keyword = e.target.value.toLowerCase();
                     const container = filterSelectRef.current?.querySelector('[data-user-list]');
                     if (!container) return;
                     container.querySelectorAll('[data-user-item]').forEach((el) => {
                       const text = (el as HTMLElement).dataset.userItem?.toLowerCase() || "";
                       (el as HTMLElement).style.display = text.includes(keyword) ? "" : "none";
                     });
                   }}
                 />

                 {users
                   .filter((u) => !filterUserIds.includes(u.id))
                   .map((u) => (
                     <button
                       key={u.id}
                       type="button"
                       data-user-item={`${u.name} ${u.badgeNo}`}
                       className="w-full text-left px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100 rounded"
                       onClick={() => {
                         setFilterUserIds((prev) => [...prev, u.id]);
                         setFilterUserSelectOpen(false);
                         setCurrentPage(1);
                       }}
                     >
                       {u.name}（{u.badgeNo}）
                     </button>
                   ))}
                 {users.filter((u) => !filterUserIds.includes(u.id)).length === 0 && (

无可用人员

                 )}

           )}

         {filterUserIds.length > 0 && (
           <button
             type="button"
             onClick={() => {
               setFilterUserIds([]);
               setFilterRole("ALL");
               setCurrentPage(1);
             }}
             className="ml-auto text-slate-400 hover:text-slate-600"
           >
             清除筛选
           </button>
         )}