|
|
@@ -0,0 +1,478 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import {
|
|
|
+ Dialog,
|
|
|
+ DialogContent,
|
|
|
+ DialogHeader,
|
|
|
+ DialogTitle,
|
|
|
+ DialogFooter,
|
|
|
+} from '@d8d/shared-ui-components/components/ui/dialog';
|
|
|
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
|
|
|
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
|
|
|
+import {
|
|
|
+ Select,
|
|
|
+ SelectContent,
|
|
|
+ SelectItem,
|
|
|
+ SelectTrigger,
|
|
|
+ SelectValue,
|
|
|
+} from '@d8d/shared-ui-components/components/ui/select';
|
|
|
+import { Label } from '@d8d/shared-ui-components/components/ui/label';
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
|
|
|
+import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
|
|
|
+import { Checkbox } from '@d8d/shared-ui-components/components/ui/checkbox';
|
|
|
+import { Alert, AlertDescription } from '@d8d/shared-ui-components/components/ui/alert';
|
|
|
+import { AlertCircle } from 'lucide-react';
|
|
|
+import { AreaSelect } from '@d8d/area-management-ui';
|
|
|
+import { disabilityClient } from '../api/disabilityClient';
|
|
|
+import type {
|
|
|
+ DisabledPersonData,
|
|
|
+ AreaSelection,
|
|
|
+} from '../api/types';
|
|
|
+
|
|
|
+interface DisabledPersonSelectorProps {
|
|
|
+ open: boolean;
|
|
|
+ onOpenChange: (open: boolean) => void;
|
|
|
+ onSelect: (person: DisabledPersonData | DisabledPersonData[]) => void;
|
|
|
+ mode?: 'single' | 'multiple';
|
|
|
+ selectedIds?: number[];
|
|
|
+ disabledIds?: number[];
|
|
|
+}
|
|
|
+
|
|
|
+const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
+ open,
|
|
|
+ onOpenChange,
|
|
|
+ onSelect,
|
|
|
+ mode = 'single',
|
|
|
+ disabledIds = [],
|
|
|
+}) => {
|
|
|
+ const [searchParams, setSearchParams] = useState<{
|
|
|
+ keyword: string;
|
|
|
+ page: number;
|
|
|
+ pageSize: number;
|
|
|
+ }>({
|
|
|
+ keyword: '',
|
|
|
+ page: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ });
|
|
|
+ const [selectedPersons, setSelectedPersons] = useState<DisabledPersonData[]>([]);
|
|
|
+ const [areaSelection, setAreaSelection] = useState<AreaSelection>({});
|
|
|
+ const [showBlacklistConfirm, setShowBlacklistConfirm] = useState(false);
|
|
|
+ const [pendingSelection, setPendingSelection] = useState<DisabledPersonData | DisabledPersonData[] | null>(null);
|
|
|
+
|
|
|
+ // 搜索残疾人列表
|
|
|
+ const { data, isLoading, refetch } = useQuery({
|
|
|
+ queryKey: ['disabled-persons-search', searchParams],
|
|
|
+ queryFn: async () => {
|
|
|
+ const response = await disabilityClient.searchDisabledPersons.$get({
|
|
|
+ query: {
|
|
|
+ keyword: searchParams.keyword || '',
|
|
|
+ skip: ((searchParams.page || 1) - 1) * (searchParams.pageSize || 10),
|
|
|
+ take: searchParams.pageSize || 10,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('搜索残疾人失败');
|
|
|
+ return await response.json();
|
|
|
+ },
|
|
|
+ enabled: open && !!searchParams.keyword,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取所有残疾人列表(当没有搜索关键词时)
|
|
|
+ const { data: allData, isLoading: isLoadingAll } = useQuery({
|
|
|
+ queryKey: ['disabled-persons-all', searchParams.page, searchParams.pageSize],
|
|
|
+ queryFn: async () => {
|
|
|
+ const response = await disabilityClient.getAllDisabledPersons.$get({
|
|
|
+ query: {
|
|
|
+ skip: ((searchParams.page || 1) - 1) * (searchParams.pageSize || 10),
|
|
|
+ take: searchParams.pageSize || 10,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('获取残疾人列表失败');
|
|
|
+ return await response.json();
|
|
|
+ },
|
|
|
+ enabled: open && !searchParams.keyword,
|
|
|
+ });
|
|
|
+
|
|
|
+ const personsData = searchParams.keyword ? data : allData;
|
|
|
+ const isLoadingData = searchParams.keyword ? isLoading : isLoadingAll;
|
|
|
+
|
|
|
+ // 重置选择器状态
|
|
|
+ useEffect(() => {
|
|
|
+ if (!open) {
|
|
|
+ setSearchParams({ keyword: '', page: 1, pageSize: 10 });
|
|
|
+ setSelectedPersons([]);
|
|
|
+ setAreaSelection({});
|
|
|
+ setShowBlacklistConfirm(false);
|
|
|
+ setPendingSelection(null);
|
|
|
+ }
|
|
|
+ }, [open]);
|
|
|
+
|
|
|
+ // 处理搜索
|
|
|
+ const handleSearch = () => {
|
|
|
+ refetch();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理重置搜索
|
|
|
+ const handleResetSearch = () => {
|
|
|
+ setSearchParams({
|
|
|
+ keyword: '',
|
|
|
+ page: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ });
|
|
|
+ setAreaSelection({});
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理选择人员
|
|
|
+ const handleSelectPerson = (person: DisabledPersonData) => {
|
|
|
+ if (disabledIds.includes(person.id)) {
|
|
|
+ return; // 跳过禁用的人员
|
|
|
+ }
|
|
|
+
|
|
|
+ if (person.isInBlackList === 1) {
|
|
|
+ // 黑名单人员需要二次确认
|
|
|
+ setPendingSelection(person);
|
|
|
+ setShowBlacklistConfirm(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mode === 'single') {
|
|
|
+ onSelect(person);
|
|
|
+ onOpenChange(false);
|
|
|
+ } else {
|
|
|
+ const isSelected = selectedPersons.some(p => p.id === person.id);
|
|
|
+ let newSelectedPersons: DisabledPersonData[];
|
|
|
+
|
|
|
+ if (isSelected) {
|
|
|
+ newSelectedPersons = selectedPersons.filter(p => p.id !== person.id);
|
|
|
+ } else {
|
|
|
+ newSelectedPersons = [...selectedPersons, person];
|
|
|
+ }
|
|
|
+
|
|
|
+ setSelectedPersons(newSelectedPersons);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理批量选择
|
|
|
+ const handleBatchSelect = () => {
|
|
|
+ if (selectedPersons.length === 0) return;
|
|
|
+
|
|
|
+ // 检查是否有黑名单人员
|
|
|
+ const blacklistPersons = selectedPersons.filter(p => p.isInBlackList === 1);
|
|
|
+ if (blacklistPersons.length > 0) {
|
|
|
+ setPendingSelection(selectedPersons);
|
|
|
+ setShowBlacklistConfirm(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ onSelect(selectedPersons);
|
|
|
+ onOpenChange(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 确认选择黑名单人员
|
|
|
+ const handleConfirmBlacklistSelection = () => {
|
|
|
+ if (pendingSelection) {
|
|
|
+ onSelect(pendingSelection);
|
|
|
+ onOpenChange(false);
|
|
|
+ }
|
|
|
+ setShowBlacklistConfirm(false);
|
|
|
+ setPendingSelection(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 取消选择黑名单人员
|
|
|
+ const handleCancelBlacklistSelection = () => {
|
|
|
+ setShowBlacklistConfirm(false);
|
|
|
+ setPendingSelection(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理分页变化
|
|
|
+ const handlePageChange = (page: number, pageSize: number) => {
|
|
|
+ setSearchParams(prev => ({ ...prev, page, pageSize }));
|
|
|
+ };
|
|
|
+
|
|
|
+ // 表格列定义
|
|
|
+ const columns = [
|
|
|
+ { key: 'name', label: '姓名' },
|
|
|
+ { key: 'gender', label: '性别' },
|
|
|
+ { key: 'idCard', label: '身份证号' },
|
|
|
+ { key: 'disabilityId', label: '残疾证号' },
|
|
|
+ { key: 'phone', label: '联系电话' },
|
|
|
+ { key: 'province', label: '省份' },
|
|
|
+ { key: 'city', label: '城市' },
|
|
|
+ { key: 'disabilityType', label: '残疾类型' },
|
|
|
+ { key: 'disabilityLevel', label: '残疾等级' },
|
|
|
+ { key: 'blacklist', label: '黑名单' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
+ <DialogContent className="max-w-6xl max-h-[80vh] overflow-hidden flex flex-col">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>选择残疾人</DialogTitle>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ {/* 搜索区域 */}
|
|
|
+ <div className="space-y-4 p-4 border rounded-lg">
|
|
|
+ <div className="grid grid-cols-4 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="name">姓名</Label>
|
|
|
+ <Input
|
|
|
+ id="name"
|
|
|
+ placeholder="输入姓名"
|
|
|
+ value={searchParams.keyword || ''}
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, keyword: e.target.value }))}
|
|
|
+ data-testid="search-name-input"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="gender">性别</Label>
|
|
|
+ <Select
|
|
|
+ value={searchParams.keyword?.includes('男') ? '男' : searchParams.keyword?.includes('女') ? '女' : ''}
|
|
|
+ onValueChange={(value) => {
|
|
|
+ if (value) {
|
|
|
+ setSearchParams(prev => ({ ...prev, keyword: value }));
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="选择性别" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="男">男</SelectItem>
|
|
|
+ <SelectItem value="女">女</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="disabilityId">残疾证号</Label>
|
|
|
+ <Input
|
|
|
+ id="disabilityId"
|
|
|
+ placeholder="输入残疾证号"
|
|
|
+ value={searchParams.keyword || ''}
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, keyword: e.target.value }))}
|
|
|
+ data-testid="search-disability-id-input"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="phone">联系电话</Label>
|
|
|
+ <Input
|
|
|
+ id="phone"
|
|
|
+ placeholder="输入联系电话"
|
|
|
+ value={searchParams.keyword || ''}
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, keyword: e.target.value }))}
|
|
|
+ data-testid="search-phone-input"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid grid-cols-4 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>省份/城市</Label>
|
|
|
+ <AreaSelect
|
|
|
+ value={areaSelection}
|
|
|
+ onChange={setAreaSelection}
|
|
|
+ data-testid="area-select"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="disabilityType">残疾类型</Label>
|
|
|
+ <Select
|
|
|
+ value={searchParams.keyword || ''}
|
|
|
+ onValueChange={(value) => {
|
|
|
+ if (value) {
|
|
|
+ setSearchParams(prev => ({ ...prev, keyword: value }));
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="选择残疾类型" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="视力残疾">视力残疾</SelectItem>
|
|
|
+ <SelectItem value="听力残疾">听力残疾</SelectItem>
|
|
|
+ <SelectItem value="言语残疾">言语残疾</SelectItem>
|
|
|
+ <SelectItem value="肢体残疾">肢体残疾</SelectItem>
|
|
|
+ <SelectItem value="智力残疾">智力残疾</SelectItem>
|
|
|
+ <SelectItem value="精神残疾">精神残疾</SelectItem>
|
|
|
+ <SelectItem value="多重残疾">多重残疾</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="disabilityLevel">残疾等级</Label>
|
|
|
+ <Select
|
|
|
+ value={searchParams.keyword || ''}
|
|
|
+ onValueChange={(value) => {
|
|
|
+ if (value) {
|
|
|
+ setSearchParams(prev => ({ ...prev, keyword: value }));
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="选择残疾等级" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="一级">一级</SelectItem>
|
|
|
+ <SelectItem value="二级">二级</SelectItem>
|
|
|
+ <SelectItem value="三级">三级</SelectItem>
|
|
|
+ <SelectItem value="四级">四级</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-end space-x-2">
|
|
|
+ <Button onClick={handleSearch} data-testid="search-button">
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ <Button variant="outline" onClick={handleResetSearch} data-testid="reset-button">
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表格区域 */}
|
|
|
+ <div className="flex-1 overflow-auto">
|
|
|
+ {isLoadingData ? (
|
|
|
+ <div className="flex items-center justify-center h-64">
|
|
|
+ <div className="text-center">加载中...</div>
|
|
|
+ </div>
|
|
|
+ ) : personsData?.data && personsData.data.length > 0 ? (
|
|
|
+ <>
|
|
|
+ <div className="border rounded-md">
|
|
|
+ <Table data-testid="disabled-persons-table">
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ {mode === 'multiple' && (
|
|
|
+ <TableHead className="w-12">
|
|
|
+ <Checkbox
|
|
|
+ checked={selectedPersons.length === personsData.data.length}
|
|
|
+ onCheckedChange={(checked) => {
|
|
|
+ if (checked) {
|
|
|
+ const selectablePersons = personsData.data.filter(
|
|
|
+ (person: DisabledPersonData) => !disabledIds.includes(person.id)
|
|
|
+ );
|
|
|
+ setSelectedPersons(selectablePersons);
|
|
|
+ } else {
|
|
|
+ setSelectedPersons([]);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ aria-label="全选"
|
|
|
+ />
|
|
|
+ </TableHead>
|
|
|
+ )}
|
|
|
+ {columns.map((column) => (
|
|
|
+ <TableHead key={column.key}>{column.label}</TableHead>
|
|
|
+ ))}
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {personsData.data.map((person: DisabledPersonData) => (
|
|
|
+ <TableRow
|
|
|
+ key={person.id}
|
|
|
+ onClick={() => mode === 'single' && handleSelectPerson(person)}
|
|
|
+ className={mode === 'single' ? 'cursor-pointer hover:bg-muted' : ''}
|
|
|
+ data-testid={`table-row-${person.id}`}
|
|
|
+ >
|
|
|
+ {mode === 'multiple' && (
|
|
|
+ <TableCell>
|
|
|
+ <Checkbox
|
|
|
+ checked={selectedPersons.some(p => p.id === person.id)}
|
|
|
+ onCheckedChange={(checked) => {
|
|
|
+ if (checked) {
|
|
|
+ setSelectedPersons([...selectedPersons, person]);
|
|
|
+ } else {
|
|
|
+ setSelectedPersons(selectedPersons.filter(p => p.id !== person.id));
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ disabled={disabledIds.includes(person.id)}
|
|
|
+ aria-label="选择"
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ )}
|
|
|
+ <TableCell>{person.name}</TableCell>
|
|
|
+ <TableCell>{person.gender}</TableCell>
|
|
|
+ <TableCell>{person.idCard}</TableCell>
|
|
|
+ <TableCell>{person.disabilityId}</TableCell>
|
|
|
+ <TableCell>{person.phone}</TableCell>
|
|
|
+ <TableCell>{person.province}</TableCell>
|
|
|
+ <TableCell>{person.city}</TableCell>
|
|
|
+ <TableCell>{person.disabilityType}</TableCell>
|
|
|
+ <TableCell>{person.disabilityLevel}</TableCell>
|
|
|
+ <TableCell>{person.isInBlackList === 1 ? '是' : '否'}</TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ <DataTablePagination
|
|
|
+ currentPage={searchParams.page || 1}
|
|
|
+ pageSize={searchParams.pageSize || 10}
|
|
|
+ totalCount={personsData.total || 0}
|
|
|
+ onPageChange={handlePageChange}
|
|
|
+ data-testid="pagination"
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <div className="flex items-center justify-center h-64">
|
|
|
+ <div className="text-center">暂无数据</div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 底部操作区域 */}
|
|
|
+ <DialogFooter className="flex justify-between">
|
|
|
+ <div className="text-sm text-muted-foreground">
|
|
|
+ {mode === 'multiple' && (
|
|
|
+ <span>已选择 {selectedPersons.length} 人</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="space-x-2">
|
|
|
+ <Button variant="outline" onClick={() => onOpenChange(false)} data-testid="cancel-button">
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ {mode === 'multiple' && (
|
|
|
+ <Button
|
|
|
+ onClick={handleBatchSelect}
|
|
|
+ disabled={selectedPersons.length === 0}
|
|
|
+ data-testid="confirm-batch-button"
|
|
|
+ >
|
|
|
+ 确认选择 ({selectedPersons.length})
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </DialogFooter>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ {/* 黑名单确认对话框 */}
|
|
|
+ <Dialog open={showBlacklistConfirm} onOpenChange={setShowBlacklistConfirm}>
|
|
|
+ <DialogContent>
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>确认选择黑名单人员</DialogTitle>
|
|
|
+ </DialogHeader>
|
|
|
+ <Alert variant="destructive">
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
+ <AlertDescription>
|
|
|
+ 您选择的人员在黑名单中,是否确认选择?
|
|
|
+ </AlertDescription>
|
|
|
+ </Alert>
|
|
|
+ <DialogFooter>
|
|
|
+ <Button variant="outline" onClick={handleCancelBlacklistSelection} data-testid="cancel-blacklist-button">
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleConfirmBlacklistSelection} data-testid="confirm-blacklist-button">
|
|
|
+ 确认选择
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default DisabledPersonSelector;
|