diff --git a/src/components/exam/ExamSection.tsx b/src/components/exam/ExamSection.tsx index aa5580a..efc946e 100644 --- a/src/components/exam/ExamSection.tsx +++ b/src/components/exam/ExamSection.tsx @@ -1,9 +1,9 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import type { ExamClient, ExamModalTab } from '../../data/mockData'; import { EXAM_TAGS } from '../../data/mockData'; import { getTodayExamProgress } from '../../api'; import { isExamActionDone } from '../../utils/examActions'; -import { Badge, Card, CardContent, CardHeader, InfoCard } from '../ui'; +import { Badge, Card, CardContent, CardHeader, InfoCard, Input } from '../ui'; import { cls } from '../../utils/cls'; interface ExamSectionProps { @@ -12,14 +12,23 @@ interface ExamSectionProps { examFilterTag: (typeof EXAM_TAGS)[number]; onFilterChange: (tag: (typeof EXAM_TAGS)[number]) => void; onOpenModal: (id: string, tab: ExamModalTab) => void; + searchValue: string; + onSearchChange: (value: string) => void; + loading?: boolean; } +const INITIAL_LOAD_COUNT = 9; // 初始加载数量(3列 x 3行) +const LOAD_MORE_COUNT = 9; // 每次加载更多时的数量 + export const ExamSection = ({ filteredClients, selectedExamClient, examFilterTag, onFilterChange, onOpenModal, + searchValue, + onSearchChange, + loading = false, }: ExamSectionProps) => { const [progressStats, setProgressStats] = useState([ { label: '预约人数', value: 0 }, @@ -27,6 +36,9 @@ export const ExamSection = ({ { label: '体检中', value: 0 }, { label: '用餐', value: 0 }, ]); + const [displayCount, setDisplayCount] = useState(INITIAL_LOAD_COUNT); + const observerTarget = useRef(null); + const [isLoadingMore, setIsLoadingMore] = useState(false); useEffect(() => { getTodayExamProgress({}) @@ -44,6 +56,49 @@ export const ExamSection = ({ }); }, []); + // 当 filteredClients 变化时,重置显示数量 + useEffect(() => { + setDisplayCount(INITIAL_LOAD_COUNT); + }, [filteredClients.length, searchValue, examFilterTag]); + + // 懒加载:使用 Intersection Observer 监听底部元素 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !loading && !isLoadingMore) { + const hasMore = displayCount < filteredClients.length; + if (hasMore) { + setIsLoadingMore(true); + // 模拟加载延迟,让用户看到 loading 效果 + setTimeout(() => { + setDisplayCount((prev) => Math.min(prev + LOAD_MORE_COUNT, filteredClients.length)); + setIsLoadingMore(false); + }, 300); + } + } + }, + { + root: null, + rootMargin: '100px', + threshold: 0.1, + } + ); + + const currentTarget = observerTarget.current; + if (currentTarget) { + observer.observe(currentTarget); + } + + return () => { + if (currentTarget) { + observer.unobserve(currentTarget); + } + }; + }, [displayCount, filteredClients.length, loading, isLoadingMore]); + + const displayedClients = filteredClients.slice(0, displayCount); + const hasMore = displayCount < filteredClients.length; + return (
@@ -56,123 +111,192 @@ export const ExamSection = ({
- +
+ onSearchChange(e.target.value)} + className='text-sm' + /> +
- 体检客户列表 -
- {EXAM_TAGS.map((tag) => ( - - ))} +
+ 体检客户列表 +
+
+ {EXAM_TAGS.map((tag) => ( + + ))} +
+
- {filteredClients.length === 0 ? ( + {loading && filteredClients.length === 0 ? ( +
+
+ + + + + 加载中... +
+
+ ) : filteredClients.length === 0 ? (
暂无体检客户数据
请尝试调整筛选条件或搜索关键词
) : ( -
- {filteredClients.map((client) => { - // 检查操作记录:优先使用 localStorage 记录,如果没有则使用原有逻辑 - const idCardSignInDone = isExamActionDone(client.id, 'idCardSignIn'); - const printSignDone = isExamActionDone(client.id, 'printSign'); + <> +
+ {displayedClients.map((client) => { + // 检查操作记录:优先使用 localStorage 记录,如果没有则使用原有逻辑 + const idCardSignInDone = isExamActionDone(client.id, 'idCardSignIn'); + const printSignDone = isExamActionDone(client.id, 'printSign'); - const signDone = idCardSignInDone || client.signStatus === '已登记' || client.checkedItems.includes('签到'); - const addonCount = client.addonCount || 0; - const printDone = printSignDone || !!client.guidePrinted; - const openModal = (tab: ExamModalTab) => onOpenModal(client.id, tab); + const signDone = idCardSignInDone || client.signStatus === '已登记' || client.checkedItems.includes('签到'); + const addonCount = client.addonCount || 0; + const printDone = printSignDone || !!client.guidePrinted; + const openModal = (tab: ExamModalTab) => onOpenModal(client.id, tab); - return ( -
openModal('detail')} - className={cls( - 'text-left p-3 rounded-2xl border bg-white hover:bg-gray-50 flex flex-col gap-1 cursor-pointer', - selectedExamClient?.id === client.id && 'border-gray-900 bg-gray-50', - )} - > -
- {client.name} - {client.level} -
-
套餐:{client.packageName}
-
- 状态:{client.status} - 已耗时:{client.elapsed} -
-
- - - - - + return ( +
openModal('detail')} + className={cls( + 'text-left p-3 rounded-2xl border bg-white hover:bg-gray-50 flex flex-col gap-1 cursor-pointer', + selectedExamClient?.id === client.id && 'border-gray-900 bg-gray-50', + )} + > +
+ {client.name} + {client.level} +
+
套餐:{client.packageName}
+
+ 状态:{client.status} + 已耗时:{client.elapsed} +
+
+ + + + + +
-
- ); - })} -
+ ); + })} +
+ {/* 懒加载触发区域和 loading */} + {(hasMore || isLoadingMore) && ( +
+ {isLoadingMore && ( +
+ + + + + 加载更多... +
+ )} +
+ )} + )} diff --git a/src/pages/ExamPage.tsx b/src/pages/ExamPage.tsx index 7beb065..496210a 100644 --- a/src/pages/ExamPage.tsx +++ b/src/pages/ExamPage.tsx @@ -1,20 +1,19 @@ import { useEffect, useMemo, useState } from 'react'; -import { useOutletContext } from 'react-router-dom'; import type { ExamClient, ExamModalTab } from '../data/mockData'; import { EXAM_TAGS } from '../data/mockData'; import { ExamSection } from '../components/exam/ExamSection'; import { ExamModal } from '../components/exam/ExamModal'; -import type { MainLayoutContext } from '../layouts/MainLayout'; import { getPhysicalExamCustomerList } from '../api'; export const ExamPage = () => { - const { search } = useOutletContext(); + const [searchValue, setSearchValue] = useState(''); const [clients, setClients] = useState([]); const [examSelectedId, setExamSelectedId] = useState(''); const [examPanelTab, setExamPanelTab] = useState('detail'); const [examModalOpen, setExamModalOpen] = useState(false); const [examFilterTag, setExamFilterTag] = useState<(typeof EXAM_TAGS)[number]>('全部'); + const [loading, setLoading] = useState(false); // 将筛选标签映射为接口 filter_type const filterType = useMemo(() => { @@ -40,14 +39,61 @@ export const ExamPage = () => { } }, [examFilterTag]); + // 智能识别搜索内容类型 + const getSearchParams = useMemo(() => { + const trimmed = searchValue.trim(); + if (!trimmed) { + return { + customer_name: undefined, + phone: undefined, + id_no: undefined, + }; + } + + // 判断是否为手机号(11位数字,以1开头) + if (/^1[3-9]\d{9}$/.test(trimmed)) { + return { + customer_name: undefined, + phone: trimmed, + id_no: undefined, + }; + } + + // 判断是否为身份证号(15位或18位,最后一位可能是X) + if (/^(\d{15}|\d{17}[\dXx])$/.test(trimmed)) { + return { + customer_name: undefined, + phone: undefined, + id_no: trimmed, + }; + } + + // 判断是否为卡号(纯数字,长度在6-20位之间) + if (/^\d{6,20}$/.test(trimmed)) { + // 接口中没有卡号字段,暂时使用 customer_name 搜索 + // 如果后续接口支持卡号字段,可以添加 card_no 参数 + return { + customer_name: trimmed, + phone: undefined, + id_no: undefined, + }; + } + + // 默认为姓名 + return { + customer_name: trimmed, + phone: undefined, + id_no: undefined, + }; + }, [searchValue]); + // 从接口拉取体检客户列表 useEffect(() => { const payload = { - customer_name: search.trim() || undefined, - phone: undefined, - id_no: undefined, + ...getSearchParams, filter_type: filterType, }; + setLoading(true); getPhysicalExamCustomerList(payload) .then((res) => { const list = res.Data || []; @@ -96,8 +142,11 @@ export const ExamPage = () => { }) .catch((err) => { console.error('获取体检客户列表失败', err); + }) + .finally(() => { + setLoading(false); }); - }, [search, filterType, examSelectedId]); + }, [getSearchParams, filterType, examSelectedId]); const selectedExamClient: ExamClient | undefined = useMemo( () => clients.find((c) => c.id === examSelectedId) || clients[0], @@ -118,6 +167,9 @@ export const ExamPage = () => { examFilterTag={examFilterTag} onFilterChange={setExamFilterTag} onOpenModal={handleOpenModal} + searchValue={searchValue} + onSearchChange={setSearchValue} + loading={loading} /> {examModalOpen && selectedExamClient && (