|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
import {
|
|
import {
|
|
|
Dialog,
|
|
Dialog,
|
|
@@ -31,6 +31,51 @@ import type {
|
|
|
AreaSelection,
|
|
AreaSelection,
|
|
|
} from '../api/types';
|
|
} from '../api/types';
|
|
|
|
|
|
|
|
|
|
+// 黑名单状态常量
|
|
|
|
|
+const BLACKLIST_STATUS = {
|
|
|
|
|
+ IN_BLACKLIST: 1,
|
|
|
|
|
+ NOT_IN_BLACKLIST: 0,
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+// 默认分页配置
|
|
|
|
|
+const DEFAULT_PAGINATION = {
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+// 默认搜索参数
|
|
|
|
|
+const createDefaultSearchParams = () => ({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ gender: '',
|
|
|
|
|
+ disabilityId: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ disabilityType: '',
|
|
|
|
|
+ disabilityLevel: '',
|
|
|
|
|
+ ...DEFAULT_PAGINATION,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 残疾类型选项
|
|
|
|
|
+const DISABILITY_TYPES = [
|
|
|
|
|
+ '视力残疾',
|
|
|
|
|
+ '听力残疾',
|
|
|
|
|
+ '言语残疾',
|
|
|
|
|
+ '肢体残疾',
|
|
|
|
|
+ '智力残疾',
|
|
|
|
|
+ '精神残疾',
|
|
|
|
|
+ '多重残疾',
|
|
|
|
|
+] as const;
|
|
|
|
|
+
|
|
|
|
|
+// 残疾等级选项
|
|
|
|
|
+const DISABILITY_LEVELS = ['一级', '二级', '三级', '四级'] as const;
|
|
|
|
|
+
|
|
|
|
|
+// 表格列定义类型
|
|
|
|
|
+type ColumnKey = 'name' | 'gender' | 'idCard' | 'disabilityId' | 'phone' | 'province' | 'city' | 'disabilityType' | 'disabilityLevel' | 'blacklist';
|
|
|
|
|
+
|
|
|
|
|
+interface ColumnDef {
|
|
|
|
|
+ key: ColumnKey;
|
|
|
|
|
+ label: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 身份证号脱敏处理
|
|
* 身份证号脱敏处理
|
|
|
* 只显示前6位和后4位,中间用****代替
|
|
* 只显示前6位和后4位,中间用****代替
|
|
@@ -42,6 +87,18 @@ const maskIdCard = (idCard: string): string => {
|
|
|
return `${idCard.slice(0, 6)}****${idCard.slice(-4)}`;
|
|
return `${idCard.slice(0, 6)}****${idCard.slice(-4)}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 搜索参数类型
|
|
|
|
|
+interface SearchParams {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ gender: string;
|
|
|
|
|
+ disabilityId: string;
|
|
|
|
|
+ phone: string;
|
|
|
|
|
+ disabilityType: string;
|
|
|
|
|
+ disabilityLevel: string;
|
|
|
|
|
+ page: number;
|
|
|
|
|
+ pageSize: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface DisabledPersonSelectorProps {
|
|
interface DisabledPersonSelectorProps {
|
|
|
open: boolean;
|
|
open: boolean;
|
|
|
onOpenChange: (open: boolean) => void;
|
|
onOpenChange: (open: boolean) => void;
|
|
@@ -50,6 +107,17 @@ interface DisabledPersonSelectorProps {
|
|
|
disabledIds?: number[];
|
|
disabledIds?: number[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 残疾人选择器组件
|
|
|
|
|
+ *
|
|
|
|
|
+ * 功能:
|
|
|
|
|
+ * - 支持单选和多选模式
|
|
|
|
|
+ * - 多字段组合搜索(姓名、性别、残疾证号、电话、残疾类型/等级、地区)
|
|
|
|
|
+ * - 分页展示残疾人列表
|
|
|
|
|
+ * - 黑名单人员二次确认
|
|
|
|
|
+ * - 身份证号脱敏显示
|
|
|
|
|
+ * - 响应式设计,支持移动端
|
|
|
|
|
+ */
|
|
|
const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
open,
|
|
open,
|
|
|
onOpenChange,
|
|
onOpenChange,
|
|
@@ -58,25 +126,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
disabledIds = [],
|
|
disabledIds = [],
|
|
|
}) => {
|
|
}) => {
|
|
|
// 多字段搜索状态管理
|
|
// 多字段搜索状态管理
|
|
|
- const [searchParams, setSearchParams] = useState<{
|
|
|
|
|
- name: string;
|
|
|
|
|
- gender: string;
|
|
|
|
|
- disabilityId: string;
|
|
|
|
|
- phone: string;
|
|
|
|
|
- disabilityType: string;
|
|
|
|
|
- disabilityLevel: string;
|
|
|
|
|
- page: number;
|
|
|
|
|
- pageSize: number;
|
|
|
|
|
- }>({
|
|
|
|
|
- name: '',
|
|
|
|
|
- gender: '',
|
|
|
|
|
- disabilityId: '',
|
|
|
|
|
- phone: '',
|
|
|
|
|
- disabilityType: '',
|
|
|
|
|
- disabilityLevel: '',
|
|
|
|
|
- page: 1,
|
|
|
|
|
- pageSize: 10,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const [searchParams, setSearchParams] = useState<SearchParams>(createDefaultSearchParams);
|
|
|
const [selectedPersons, setSelectedPersons] = useState<DisabledPersonData[]>([]);
|
|
const [selectedPersons, setSelectedPersons] = useState<DisabledPersonData[]>([]);
|
|
|
|
|
|
|
|
const [areaSelection, setAreaSelection] = useState<AreaSelection>({});
|
|
const [areaSelection, setAreaSelection] = useState<AreaSelection>({});
|
|
@@ -88,8 +138,8 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
// 为AreaSelect创建表单上下文
|
|
// 为AreaSelect创建表单上下文
|
|
|
const form = useForm();
|
|
const form = useForm();
|
|
|
|
|
|
|
|
- // 搜索残疾人列表
|
|
|
|
|
- const { data, isLoading } = useQuery({
|
|
|
|
|
|
|
+ // 搜索残疾人列表(添加错误处理)
|
|
|
|
|
+ const { data, isLoading, error } = useQuery({
|
|
|
queryKey: ['disabled-persons-search', searchParams, searchAreaNames],
|
|
queryKey: ['disabled-persons-search', searchParams, searchAreaNames],
|
|
|
queryFn: async () => {
|
|
queryFn: async () => {
|
|
|
// 组合多个搜索字段为keyword
|
|
// 组合多个搜索字段为keyword
|
|
@@ -110,8 +160,8 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
const response = await disabilityClientManager.get().searchDisabledPersons.$get({
|
|
const response = await disabilityClientManager.get().searchDisabledPersons.$get({
|
|
|
query: {
|
|
query: {
|
|
|
keyword: keyword || '',
|
|
keyword: keyword || '',
|
|
|
- skip: ((searchParams.page || 1) - 1) * (searchParams.pageSize || 10),
|
|
|
|
|
- take: searchParams.pageSize || 10,
|
|
|
|
|
|
|
+ skip: ((searchParams.page || DEFAULT_PAGINATION.page) - 1) * (searchParams.pageSize || DEFAULT_PAGINATION.pageSize),
|
|
|
|
|
+ take: searchParams.pageSize || DEFAULT_PAGINATION.pageSize,
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
if (response.status !== 200) throw new Error('搜索残疾人失败');
|
|
if (response.status !== 200) throw new Error('搜索残疾人失败');
|
|
@@ -123,19 +173,10 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
const personsData = data;
|
|
const personsData = data;
|
|
|
const isLoadingData = isLoading;
|
|
const isLoadingData = isLoading;
|
|
|
|
|
|
|
|
- // 重置选择器状态
|
|
|
|
|
|
|
+ // 重置选择器状态(使用 useCallback 优化)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (!open) {
|
|
if (!open) {
|
|
|
- setSearchParams({
|
|
|
|
|
- name: '',
|
|
|
|
|
- gender: '',
|
|
|
|
|
- disabilityId: '',
|
|
|
|
|
- phone: '',
|
|
|
|
|
- disabilityType: '',
|
|
|
|
|
- disabilityLevel: '',
|
|
|
|
|
- page: 1,
|
|
|
|
|
- pageSize: 10,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ setSearchParams(createDefaultSearchParams());
|
|
|
setSelectedPersons([]);
|
|
setSelectedPersons([]);
|
|
|
setAreaSelection({});
|
|
setAreaSelection({});
|
|
|
setSearchAreaNames({});
|
|
setSearchAreaNames({});
|
|
@@ -144,34 +185,25 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
}
|
|
}
|
|
|
}, [open]);
|
|
}, [open]);
|
|
|
|
|
|
|
|
- // 处理搜索
|
|
|
|
|
- const handleSearch = () => {
|
|
|
|
|
- setSearchParams(prev => ({ ...prev, page: 1 }));
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 处理重置搜索
|
|
|
|
|
- const handleResetSearch = () => {
|
|
|
|
|
- setSearchParams({
|
|
|
|
|
- name: '',
|
|
|
|
|
- gender: '',
|
|
|
|
|
- disabilityId: '',
|
|
|
|
|
- phone: '',
|
|
|
|
|
- disabilityType: '',
|
|
|
|
|
- disabilityLevel: '',
|
|
|
|
|
- page: 1,
|
|
|
|
|
- pageSize: 10,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 处理搜索(使用 useCallback 优化)
|
|
|
|
|
+ const handleSearch = useCallback(() => {
|
|
|
|
|
+ setSearchParams(prev => ({ ...prev, page: DEFAULT_PAGINATION.page }));
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理重置搜索(使用 useCallback 优化)
|
|
|
|
|
+ const handleResetSearch = useCallback(() => {
|
|
|
|
|
+ setSearchParams(createDefaultSearchParams());
|
|
|
setAreaSelection({});
|
|
setAreaSelection({});
|
|
|
setSearchAreaNames({});
|
|
setSearchAreaNames({});
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- // 处理选择人员
|
|
|
|
|
- const handleSelectPerson = (person: DisabledPersonData) => {
|
|
|
|
|
|
|
+ // 处理选择人员(使用 useCallback 优化)
|
|
|
|
|
+ const handleSelectPerson = useCallback((person: DisabledPersonData) => {
|
|
|
if (disabledIds.includes(person.id)) {
|
|
if (disabledIds.includes(person.id)) {
|
|
|
return; // 跳过禁用的人员
|
|
return; // 跳过禁用的人员
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (person.isInBlackList === 1) {
|
|
|
|
|
|
|
+ if (person.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST) {
|
|
|
// 黑名单人员需要二次确认
|
|
// 黑名单人员需要二次确认
|
|
|
setPendingSelection(person);
|
|
setPendingSelection(person);
|
|
|
setShowBlacklistConfirm(true);
|
|
setShowBlacklistConfirm(true);
|
|
@@ -193,10 +225,10 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
|
|
|
|
|
setSelectedPersons(newSelectedPersons);
|
|
setSelectedPersons(newSelectedPersons);
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ }, [disabledIds, mode, onSelect, onOpenChange, selectedPersons]);
|
|
|
|
|
|
|
|
- // 处理批量选择
|
|
|
|
|
- const handleBatchSelect = () => {
|
|
|
|
|
|
|
+ // 处理批量选择(使用 useCallback 优化)
|
|
|
|
|
+ const handleBatchSelect = useCallback(() => {
|
|
|
console.debug('[DisabledPersonSelector] handleBatchSelect 被调用, selectedPersons:', selectedPersons.length);
|
|
console.debug('[DisabledPersonSelector] handleBatchSelect 被调用, selectedPersons:', selectedPersons.length);
|
|
|
if (selectedPersons.length === 0) {
|
|
if (selectedPersons.length === 0) {
|
|
|
console.debug('[DisabledPersonSelector] 没有选中人员,返回');
|
|
console.debug('[DisabledPersonSelector] 没有选中人员,返回');
|
|
@@ -204,7 +236,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 检查是否有黑名单人员
|
|
// 检查是否有黑名单人员
|
|
|
- const blacklistPersons = selectedPersons.filter(p => p.isInBlackList === 1);
|
|
|
|
|
|
|
+ const blacklistPersons = selectedPersons.filter(p => p.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST);
|
|
|
if (blacklistPersons.length > 0) {
|
|
if (blacklistPersons.length > 0) {
|
|
|
console.debug('[DisabledPersonSelector] 检测到黑名单人员');
|
|
console.debug('[DisabledPersonSelector] 检测到黑名单人员');
|
|
|
setPendingSelection(selectedPersons);
|
|
setPendingSelection(selectedPersons);
|
|
@@ -215,31 +247,31 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
console.debug('[DisabledPersonSelector] 调用 onSelect, 人员数量:', selectedPersons.length);
|
|
console.debug('[DisabledPersonSelector] 调用 onSelect, 人员数量:', selectedPersons.length);
|
|
|
onSelect(selectedPersons);
|
|
onSelect(selectedPersons);
|
|
|
onOpenChange(false);
|
|
onOpenChange(false);
|
|
|
- };
|
|
|
|
|
|
|
+ }, [onSelect, onOpenChange, selectedPersons]);
|
|
|
|
|
|
|
|
- // 确认选择黑名单人员
|
|
|
|
|
- const handleConfirmBlacklistSelection = () => {
|
|
|
|
|
|
|
+ // 确认选择黑名单人员(使用 useCallback 优化)
|
|
|
|
|
+ const handleConfirmBlacklistSelection = useCallback(() => {
|
|
|
if (pendingSelection) {
|
|
if (pendingSelection) {
|
|
|
onSelect(pendingSelection);
|
|
onSelect(pendingSelection);
|
|
|
onOpenChange(false);
|
|
onOpenChange(false);
|
|
|
}
|
|
}
|
|
|
setShowBlacklistConfirm(false);
|
|
setShowBlacklistConfirm(false);
|
|
|
setPendingSelection(null);
|
|
setPendingSelection(null);
|
|
|
- };
|
|
|
|
|
|
|
+ }, [onSelect, onOpenChange, pendingSelection]);
|
|
|
|
|
|
|
|
- // 取消选择黑名单人员
|
|
|
|
|
- const handleCancelBlacklistSelection = () => {
|
|
|
|
|
|
|
+ // 取消选择黑名单人员(使用 useCallback 优化)
|
|
|
|
|
+ const handleCancelBlacklistSelection = useCallback(() => {
|
|
|
setShowBlacklistConfirm(false);
|
|
setShowBlacklistConfirm(false);
|
|
|
setPendingSelection(null);
|
|
setPendingSelection(null);
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- // 处理分页变化
|
|
|
|
|
- const handlePageChange = (page: number, pageSize: number) => {
|
|
|
|
|
|
|
+ // 处理分页变化(使用 useCallback 优化)
|
|
|
|
|
+ const handlePageChange = useCallback((page: number, pageSize: number) => {
|
|
|
setSearchParams(prev => ({ ...prev, page, pageSize }));
|
|
setSearchParams(prev => ({ ...prev, page, pageSize }));
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- // 表格列定义
|
|
|
|
|
- const columns = [
|
|
|
|
|
|
|
+ // 表格列定义(使用 useMemo 优化)
|
|
|
|
|
+ const columns: ColumnDef[] = useMemo(() => [
|
|
|
{ key: 'name', label: '姓名' },
|
|
{ key: 'name', label: '姓名' },
|
|
|
{ key: 'gender', label: '性别' },
|
|
{ key: 'gender', label: '性别' },
|
|
|
{ key: 'idCard', label: '身份证号' },
|
|
{ key: 'idCard', label: '身份证号' },
|
|
@@ -250,7 +282,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
{ key: 'disabilityType', label: '残疾类型' },
|
|
{ key: 'disabilityType', label: '残疾类型' },
|
|
|
{ key: 'disabilityLevel', label: '残疾等级' },
|
|
{ key: 'disabilityLevel', label: '残疾等级' },
|
|
|
{ key: 'blacklist', label: '黑名单' },
|
|
{ key: 'blacklist', label: '黑名单' },
|
|
|
- ];
|
|
|
|
|
|
|
+ ], []);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
@@ -359,13 +391,9 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
<SelectValue placeholder="选择残疾类型" />
|
|
<SelectValue placeholder="选择残疾类型" />
|
|
|
</SelectTrigger>
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
<SelectContent>
|
|
|
- <SelectItem value="视力残疾">视力残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="听力残疾">听力残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="言语残疾">言语残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="肢体残疾">肢体残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="智力残疾">智力残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="精神残疾">精神残疾</SelectItem>
|
|
|
|
|
- <SelectItem value="多重残疾">多重残疾</SelectItem>
|
|
|
|
|
|
|
+ {DISABILITY_TYPES.map(type => (
|
|
|
|
|
+ <SelectItem key={type} value={type}>{type}</SelectItem>
|
|
|
|
|
+ ))}
|
|
|
</SelectContent>
|
|
</SelectContent>
|
|
|
</Select>
|
|
</Select>
|
|
|
</div>
|
|
</div>
|
|
@@ -382,10 +410,9 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
<SelectValue placeholder="选择残疾等级" />
|
|
<SelectValue placeholder="选择残疾等级" />
|
|
|
</SelectTrigger>
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
<SelectContent>
|
|
|
- <SelectItem value="一级">一级</SelectItem>
|
|
|
|
|
- <SelectItem value="二级">二级</SelectItem>
|
|
|
|
|
- <SelectItem value="三级">三级</SelectItem>
|
|
|
|
|
- <SelectItem value="四级">四级</SelectItem>
|
|
|
|
|
|
|
+ {DISABILITY_LEVELS.map(level => (
|
|
|
|
|
+ <SelectItem key={level} value={level}>{level}</SelectItem>
|
|
|
|
|
+ ))}
|
|
|
</SelectContent>
|
|
</SelectContent>
|
|
|
</Select>
|
|
</Select>
|
|
|
</div>
|
|
</div>
|
|
@@ -409,6 +436,13 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="flex items-center justify-center h-64">
|
|
|
<div className="text-center">加载中...</div>
|
|
<div className="text-center">加载中...</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ ) : error ? (
|
|
|
|
|
+ <Alert variant="destructive">
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertDescription>
|
|
|
|
|
+ 加载残疾人数据失败,请稍后重试
|
|
|
|
|
+ </AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
) : personsData?.data && personsData.data.length > 0 ? (
|
|
) : personsData?.data && personsData.data.length > 0 ? (
|
|
|
<>
|
|
<>
|
|
|
{/* 移动端提示 */}
|
|
{/* 移动端提示 */}
|
|
@@ -487,7 +521,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
|
|
|
<TableCell className="hidden lg:table-cell">{person.city}</TableCell>
|
|
<TableCell className="hidden lg:table-cell">{person.city}</TableCell>
|
|
|
<TableCell className="hidden lg:table-cell">{person.disabilityType}</TableCell>
|
|
<TableCell className="hidden lg:table-cell">{person.disabilityType}</TableCell>
|
|
|
<TableCell className="hidden lg:table-cell">{person.disabilityLevel}</TableCell>
|
|
<TableCell className="hidden lg:table-cell">{person.disabilityLevel}</TableCell>
|
|
|
- <TableCell className="hidden lg:table-cell">{person.isInBlackList === 1 ? '是' : '否'}</TableCell>
|
|
|
|
|
|
|
+ <TableCell className="hidden lg:table-cell">{person.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST ? '是' : '否'}</TableCell>
|
|
|
</TableRow>
|
|
</TableRow>
|
|
|
))}
|
|
))}
|
|
|
</TableBody>
|
|
</TableBody>
|