From e85792a237ebc02ddca6dafa7e02a314e78c3679 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Sat, 28 Mar 2026 23:38:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89SearchSelect?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E4=B8=8B=E6=8B=89?= =?UTF-8?q?+=E6=A8=A1=E7=B3=8A=E6=90=9C=E7=B4=A2+iOS=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增SearchSelect组件:输入框可打字模糊过滤,下拉列表点击选择 - 客户名称、业务负责人、车型名称、车牌号码使用SearchSelect - 短选项列表(区域/城市/品牌/部门/业务员)保持原生select - 点击外部自动关闭下拉,已选中项高亮,无匹配显示提示 - iOS Safari完全兼容(不依赖datalist) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.tsx | 102 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c8e2cd8..f60e661 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Truck, Warehouse, @@ -35,6 +35,76 @@ import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './api'; import type { WeeklyDetailItem } from './api'; +// --- SearchSelect Component --- +function SearchSelect({ value, onChange, options, placeholder, className }: { + value: string; + onChange: (v: string) => void; + options: string[]; + placeholder: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const ref = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const filtered = useMemo(() => { + if (!query) return options; + const q = query.toLowerCase(); + return options.filter((o) => o.toLowerCase().includes(q)); + }, [options, query]); + + const displayValue = value || ''; + + return ( +
+
setOpen(!open)} + > + { setQuery(e.target.value); if (!open) setOpen(true); }} + onFocus={() => { setOpen(true); setQuery(''); }} + /> + +
+ {open && ( +
+
{ onChange(''); setQuery(''); setOpen(false); }} + > + {placeholder} +
+ {filtered.map((o) => ( +
{ onChange(o); setQuery(''); setOpen(false); }} + > + {o} +
+ ))} + {filtered.length === 0 && ( +
无匹配项
+ )} +
+ )} +
+ ); +} + // --- Constants --- const TABS = [ { id: 'overview', label: '总览' }, @@ -983,10 +1053,7 @@ export default function App() {
- + setInventoryFilters({...inventoryFilters, model: v})} options={uniqueInventoryModels} placeholder="全部车型" />
@@ -1872,10 +1939,7 @@ export default function App() {
- + setRegionFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
@@ -2180,18 +2244,12 @@ export default function App() {
- + setCustomerFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
- + setCustomerFilters(prev => ({ ...prev, manager: v }))} options={uniqueCustomerManagers} placeholder="所有负责人" className="text-xs py-2 px-2" />
@@ -2466,10 +2524,7 @@ export default function App() { {/* Quick Search always visible when collapsed */} {!isModalFilterExpanded && (
e.stopPropagation()}> - + setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="快速搜索车牌..." className="text-[11px] py-1 px-2" />
)}
- + setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="全部车牌" className="text-[11px] py-1.5 px-2" />