优化加项搜索
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||||
|
|
||||||
import type { ExamClient } from '../../data/mockData';
|
import type { ExamClient } from '../../data/mockData';
|
||||||
import { searchPhysicalExamAddItem } from '../../api';
|
import { searchPhysicalExamAddItem } from '../../api';
|
||||||
@@ -24,10 +24,31 @@ interface ExamAddonPanelProps {
|
|||||||
|
|
||||||
export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
||||||
const [addonList, setAddonList] = useState<AddonItem[]>([]);
|
const [addonList, setAddonList] = useState<AddonItem[]>([]);
|
||||||
const [addonSearch, setAddonSearch] = useState('');
|
// 防抖:内部输入值(用于显示)
|
||||||
|
const [addonSearchInput, setAddonSearchInput] = useState('');
|
||||||
|
// 防抖:实际用于 API 调用的值(延迟更新)
|
||||||
|
const [debouncedAddonSearch, setDebouncedAddonSearch] = useState('');
|
||||||
|
const debounceTimerRef = useRef<number | null>(null);
|
||||||
const [addonLoading, setAddonLoading] = useState(false);
|
const [addonLoading, setAddonLoading] = useState(false);
|
||||||
const [addonError, setAddonError] = useState<string | null>(null);
|
const [addonError, setAddonError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 防抖:当输入值变化时,延迟 0.5 秒后更新 debouncedAddonSearch(用于 API 调用)
|
||||||
|
useEffect(() => {
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
window.clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimerRef.current = window.setTimeout(() => {
|
||||||
|
setDebouncedAddonSearch(addonSearchInput);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
window.clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [addonSearchInput]);
|
||||||
|
|
||||||
// 拉取加项列表
|
// 拉取加项列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const physical_exam_id = Number(client.id);
|
const physical_exam_id = Number(client.id);
|
||||||
@@ -42,7 +63,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
try {
|
try {
|
||||||
const res = await searchPhysicalExamAddItem({
|
const res = await searchPhysicalExamAddItem({
|
||||||
physical_exam_id,
|
physical_exam_id,
|
||||||
item_name: addonSearch.trim() || null,
|
item_name: debouncedAddonSearch.trim() || null,
|
||||||
});
|
});
|
||||||
if (res.Status === 200 && res.Data?.addItemList) {
|
if (res.Status === 200 && res.Data?.addItemList) {
|
||||||
const list: AddonItem[] = res.Data.addItemList.map((item) => ({
|
const list: AddonItem[] = res.Data.addItemList.map((item) => ({
|
||||||
@@ -69,7 +90,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchList();
|
fetchList();
|
||||||
}, [addonSearch, client.id]);
|
}, [debouncedAddonSearch, client.id]);
|
||||||
|
|
||||||
const allAddons = useMemo(() => addonList, [addonList]);
|
const allAddons = useMemo(() => addonList, [addonList]);
|
||||||
|
|
||||||
@@ -138,91 +159,101 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='flex items-center justify-between mb-2 gap-3'>
|
<div className='flex items-center justify-between mb-2 gap-3'>
|
||||||
<h3 className='text-lg font-semibold text-gray-900'>体检套餐加项选择</h3>
|
<h3 className='text-lg font-semibold text-gray-900'>体检套餐加项选择</h3>
|
||||||
<div className='w-[260px]'>
|
<div className='w-[260px] flex items-center gap-2'>
|
||||||
<Input
|
<Input
|
||||||
placeholder='搜索 加项名称'
|
placeholder='搜索 加项名称'
|
||||||
value={addonSearch}
|
value={addonSearchInput}
|
||||||
onChange={(e) => setAddonSearch(e.target.value)}
|
onChange={(e) => setAddonSearchInput(e.target.value)}
|
||||||
className='text-sm'
|
className='text-sm flex-1'
|
||||||
/>
|
/>
|
||||||
|
{addonSearchInput && (
|
||||||
|
<Button
|
||||||
|
className='px-3 py-1.5 text-xs whitespace-nowrap'
|
||||||
|
onClick={() => setAddonSearchInput('')}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-xs text-gray-600 space-y-1'>
|
{/* <div className='text-xs text-gray-600 space-y-1'>
|
||||||
<div>最多可选 {maxSelect} 项 · 一排 5 个</div>
|
<div>最多可选 {maxSelect} 项 · 一排 5 个</div>
|
||||||
<div>已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。</div>
|
<div>已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加项网格 */}
|
{/* 加项网格 */}
|
||||||
<div className='grid grid-cols-5 gap-3 min-h-[142px]'>
|
<div className='overflow-y-auto max-h-[366px]'>
|
||||||
{addonError && (
|
<div className='grid grid-cols-5 gap-3 min-h-[142px]'>
|
||||||
<div className='col-span-5 text-xs text-amber-600'>{addonError}</div>
|
{addonError && (
|
||||||
)}
|
<div className='col-span-5 text-xs text-amber-600'>{addonError}</div>
|
||||||
{addonLoading && (
|
)}
|
||||||
<div className='col-span-5 text-xs text-gray-500'>加载中...</div>
|
{addonLoading && (
|
||||||
)}
|
<div className='col-span-5 text-xs text-gray-500'>加载中...</div>
|
||||||
{!addonLoading && !addonError && filteredAddons.length === 0 && (
|
)}
|
||||||
<div className='col-span-5 text-xs text-gray-500'>暂无数据</div>
|
{!addonLoading && !addonError && filteredAddons.length === 0 && (
|
||||||
)}
|
<div className='col-span-5 text-xs text-gray-500'>暂无数据</div>
|
||||||
{filteredAddons.map((item) => {
|
)}
|
||||||
const id = item.id || item.name;
|
{filteredAddons.map((item) => {
|
||||||
const isSelected = selectedIds.has(id);
|
const id = item.id || item.name;
|
||||||
const origPrice = parseFloat(item.originalPrice || '0');
|
const isSelected = selectedIds.has(id);
|
||||||
const currPrice = parseFloat(item.currentPrice || '0');
|
const origPrice = parseFloat(item.originalPrice || '0');
|
||||||
|
const currPrice = parseFloat(item.currentPrice || '0');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className='border rounded-lg p-3 cursor-pointer transition-all flex flex-col'
|
className='border rounded-lg p-3 cursor-pointer transition-all flex flex-col'
|
||||||
onClick={() => toggleSelect(id)}
|
onClick={() => toggleSelect(id)}
|
||||||
>
|
>
|
||||||
{/* 第一行:复选框 + 名称 */}
|
{/* 第一行:复选框 + 名称 */}
|
||||||
<div className='flex items-start gap-2 mb-1'>
|
<div className='flex items-start gap-2 mb-1'>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => toggleSelect(id)}
|
onChange={() => toggleSelect(id)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className='mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500'
|
className='mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500'
|
||||||
/>
|
/>
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0'>
|
||||||
<div className='font-semibold text-[14px] text-gray-900'>{item.name}</div>
|
<div className='font-semibold text-[14px] text-gray-900'>{item.name}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 第二行:标签 */}
|
{/* 第二行:标签 */}
|
||||||
{item.tags && item.tags.length >= 0 && (
|
{item.tags && item.tags.length >= 0 && (
|
||||||
<div className='flex flex-wrap gap-1 mb-2 pl-6'>
|
<div className='flex flex-wrap gap-1 mb-1 pl-6'>
|
||||||
{item.tags
|
{item.tags
|
||||||
.filter(t => t.type !== 4) // 折扣信息单独显示
|
.filter(t => t.type !== 4) // 折扣信息单独显示
|
||||||
.map((tag, idx) => (
|
.map((tag, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`text-[10px] px-2 rounded-full ${getTagStyle(tag)}`}
|
className={`text-[10px] px-2 rounded-full ${getTagStyle(tag)}`}
|
||||||
>
|
>
|
||||||
{tag.title}
|
{tag.title}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 第三行:价格信息(固定在卡片底部) */}
|
||||||
|
<div className='mt-auto pt-1'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
{origPrice >= 0 && origPrice >= currPrice && (
|
||||||
|
<span className='text-xs text-gray-400 line-through'>¥{origPrice.toFixed(0)}</span>
|
||||||
|
)}
|
||||||
|
<div className='flex items-center justify-between gap-2'>
|
||||||
|
<span className='text-[14px] font-bold text-red-600'>¥{currPrice.toFixed(0)}</span>
|
||||||
|
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
|
||||||
|
{getDiscountText(item)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 第三行:价格信息(固定在卡片底部) */}
|
|
||||||
<div className='mt-auto pt-2'>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
{origPrice >= 0 && origPrice >= currPrice && (
|
|
||||||
<span className='text-xs text-gray-400 line-through'>¥{origPrice.toFixed(0)}</span>
|
|
||||||
)}
|
|
||||||
<div className='flex items-center justify-between gap-2'>
|
|
||||||
<span className='text-[14px] font-bold text-red-600'>¥{currPrice.toFixed(0)}</span>
|
|
||||||
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
|
|
||||||
{getDiscountText(item)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部汇总和支付 */}
|
{/* 底部汇总和支付 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user