重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-28 17:31:33 +08:00
parent cb5741f487
commit 763d14f3d3

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useMemo, useRef } from 'react';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,14 +8,7 @@ import {
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@ -31,13 +24,10 @@ import {
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import { import {
Plus, Plus,
Edit, Edit,
Trash2, Trash2,
Search,
Loader2, Loader2,
Users, Users,
Check, Check,
@ -53,7 +43,6 @@ import type {
TeamMemberResponse, TeamMemberResponse,
TeamMemberQuery TeamMemberQuery
} from '../../Member/types'; } from '../../Member/types';
import type { Page } from '@/types/base';
import type { UserResponse } from '@/pages/System/User/List/types'; import type { UserResponse } from '@/pages/System/User/List/types';
interface MemberManageDialogProps { interface MemberManageDialogProps {
@ -79,9 +68,8 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
onSuccess onSuccess
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const tableRef = useRef<PaginatedTableRef<TeamMemberResponse>>(null);
const [data, setData] = useState<Page<TeamMemberResponse> | null>(null); const [memberData, setMemberData] = useState<TeamMemberResponse[]>([]);
const [searchText, setSearchText] = useState('');
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<TeamMemberResponse | null>(null); const [deleteRecord, setDeleteRecord] = useState<TeamMemberResponse | null>(null);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]); const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
@ -90,48 +78,16 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editRecord, setEditRecord] = useState<TeamMemberResponse | null>(null); const [editRecord, setEditRecord] = useState<TeamMemberResponse | null>(null);
const [editRole, setEditRole] = useState(''); const [editRole, setEditRole] = useState('');
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
// 包装 fetchFn
// 加载成员列表 const fetchData = async (query: TeamMemberQuery) => {
const loadMembers = async () => { const result = await getTeamMembers({
if (!teamId) return; ...query,
teamId: teamId,
setLoading(true); userName: searchUserName || undefined,
try { });
const query: TeamMemberQuery = { setMemberData(result.content || []);
teamId: teamId, return result;
userName: searchText || undefined,
pageNum,
pageSize,
};
const result = await getTeamMembers(query);
setData(result);
} catch (error) {
console.error('加载成员失败:', error);
toast({
variant: "destructive",
title: "加载失败",
description: error instanceof Error ? error.message : '未知错误',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open) {
loadMembers();
}
}, [open, pageNum, pageSize]);
// 搜索
const handleSearch = () => {
setPageNum(0);
loadMembers();
}; };
// 切换用户选择 // 切换用户选择
@ -176,7 +132,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
setSelectedUserIds([]); setSelectedUserIds([]);
setPopoverOpen(false); setPopoverOpen(false);
loadMembers(); tableRef.current?.refresh();
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
console.error('批量添加失败:', error); console.error('批量添加失败:', error);
@ -215,7 +171,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
setEditDialogOpen(false); setEditDialogOpen(false);
setEditRecord(null); setEditRecord(null);
setEditRole(''); setEditRole('');
loadMembers(); tableRef.current?.refresh();
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
console.error('更新失败:', error); console.error('更新失败:', error);
@ -241,9 +197,46 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
// 过滤出未加入的用户(排除负责人和已加入的成员) // 过滤出未加入的用户(排除负责人和已加入的成员)
const availableUsers = users.filter(user => const availableUsers = users.filter(user =>
user.id !== ownerId && !data?.content?.some(member => member.userId === user.id) user.id !== ownerId && !memberData.some(member => member.userId === user.id)
); );
// 搜索状态
const [searchUserName, setSearchUserName] = useState('');
// 列定义
const columns: ColumnDef<TeamMemberResponse>[] = useMemo(() => [
{ key: 'userId', title: '用户ID', dataIndex: 'userId', width: '100px' },
{ key: 'userName', title: '用户名', dataIndex: 'userName', width: '150px' },
{ key: 'roleInTeam', title: '团队角色', dataIndex: 'roleInTeam', width: '150px' },
{ key: 'joinTime', title: '加入时间', dataIndex: 'joinTime', width: '180px' },
{
key: 'actions',
title: '操作',
width: '120px',
sticky: true,
render: (_, record) => (
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleEditClick(record)}
>
<Edit className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleDeleteClick(record)}
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
),
},
], []);
return ( return (
<> <>
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@ -259,17 +252,19 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
<div className="space-y-4 flex-1 overflow-y-auto"> <div className="space-y-4 flex-1 overflow-y-auto">
{/* 操作栏 */} {/* 操作栏 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative flex-1"> {/* 搜索框 */}
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Input
<Input placeholder="搜索成员姓名..."
placeholder="搜索成员姓名..." value={searchUserName}
value={searchText} onChange={(e) => setSearchUserName(e.target.value)}
onChange={(e) => setSearchText(e.target.value)} onKeyDown={(e) => {
onKeyDown={(e) => e.key === 'Enter' && handleSearch()} if (e.key === 'Enter') {
className="pl-10" tableRef.current?.refresh();
/> }
</div> }}
className="flex-1"
/>
{/* 多选下拉框 */} {/* 多选下拉框 */}
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}> <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -277,7 +272,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={popoverOpen} aria-expanded={popoverOpen}
className="w-[280px] justify-between" className="w-[200px] justify-between"
disabled={availableUsers.length === 0} disabled={availableUsers.length === 0}
> >
{selectedUserIds.length > 0 ? ( {selectedUserIds.length > 0 ? (
@ -340,89 +335,14 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
{/* 表格 */} {/* 表格 */}
<div className="border rounded-lg overflow-hidden"> <div className="border rounded-lg overflow-hidden">
<div className="max-h-[50vh] overflow-y-auto"> <PaginatedTable<TeamMemberResponse, TeamMemberQuery>
<Table minWidth="680px"> ref={tableRef}
<TableHeader> fetchFn={fetchData}
<TableRow> columns={columns}
<TableHead width="100px">ID</TableHead> rowKey="id"
<TableHead width="150px"></TableHead> minWidth="680px"
<TableHead width="150px"></TableHead> pageSize={10}
<TableHead width="160px"></TableHead> />
<TableHead width="120px" sticky></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8">
<div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : data?.content && data.content.length > 0 ? (
data.content.map((record) => (
<TableRow key={record.id}>
<TableCell width="100px" className="font-medium">
{record.userId}
</TableCell>
<TableCell width="150px">
{record.userName || '-'}
</TableCell>
<TableCell width="150px">
{record.roleInTeam || '-'}
</TableCell>
<TableCell width="160px">
{record.joinTime || '-'}
</TableCell>
<TableCell width="120px" sticky>
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleEditClick(record)}
>
<Edit className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleDeleteClick(record)}
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-3">
<Users className="h-12 w-12 opacity-20" />
<p className="text-sm"></p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.totalElements > 0 && (
<div className="flex justify-end py-3 px-4 border-t bg-muted/40">
<DataTablePagination
pageIndex={pageNum}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(newPage) => setPageNum(newPage)}
/>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -474,7 +394,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
description: `成员 "${deleteRecord?.userName}" 已移除`, description: `成员 "${deleteRecord?.userName}" 已移除`,
}); });
setDeleteRecord(null); setDeleteRecord(null);
loadMembers(); tableRef.current?.refresh();
onSuccess?.(); onSuccess?.();
}} }}
variant="destructive" variant="destructive"