import * as React from "react" import { View, Input, ScrollView } from "@tarojs/components" import Taro from "@tarojs/taro" import { Search } from "lucide-react-taro" import { cn } from "@/lib/utils" import { Dialog, DialogContent } from "@/components/ui/dialog" const CommandContext = React.createContext<{ search: string deferredSearch: string setSearch: (search: string) => void } | null>(null) const CommandItemsContext = React.createContext<{ setItemState: (id: string, state: ItemState) => void removeItem: (id: string) => void hasAnyMatch: () => boolean groupHasAnyMatch: (groupId: string) => boolean itemsSize: number } | null>(null) type ItemState = { match: boolean; groupId?: string } const GroupContext = React.createContext<{ groupId?: string } | null>(null) function getNodeText(node: React.ReactNode): string { if (node == null || typeof node === "boolean") return "" if (typeof node === "string" || typeof node === "number") return String(node) if (Array.isArray(node)) return node.map(getNodeText).join(" ") if (React.isValidElement(node)) return getNodeText(node.props?.children) return "" } const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => { const [search, setSearch] = React.useState("") // 使用 deferredSearch 来延迟搜索过滤逻辑,确保输入框在输入时保持响应,解决微信小程序中的输入抖动和文字消失问题 const deferredSearch = React.useDeferredValue(search) const [, setItemsTick] = React.useState(0) const itemsRef = React.useRef>(new Map()) const tickRef = React.useRef | null>(null) React.useEffect(() => { return () => { if (tickRef.current) clearTimeout(tickRef.current) } }, []) const triggerItemsUpdate = React.useCallback(() => { if (tickRef.current) return // 使用短延时批处理项目状态更新,减少重绘频率 tickRef.current = setTimeout(() => { setItemsTick((v) => v + 1) tickRef.current = null }, 16) }, []) const setItemState = React.useCallback((id: string, state: ItemState) => { const prev = itemsRef.current.get(id) if (prev?.match === state.match && prev?.groupId === state.groupId) return itemsRef.current.set(id, state) triggerItemsUpdate() }, [triggerItemsUpdate]) const removeItem = React.useCallback((id: string) => { if (!itemsRef.current.has(id)) return itemsRef.current.delete(id) triggerItemsUpdate() }, [triggerItemsUpdate]) const hasAnyMatch = React.useCallback(() => { for (const s of itemsRef.current.values()) { if (s.match) return true } return false }, []) const groupHasAnyMatch = React.useCallback((groupId: string) => { for (const s of itemsRef.current.values()) { if (s.groupId === groupId && s.match) return true } return false }, []) const searchContextValue = React.useMemo(() => ({ search, deferredSearch, setSearch, }), [search, deferredSearch]) const itemsContextValue = React.useMemo(() => ({ setItemState, removeItem, hasAnyMatch, groupHasAnyMatch, itemsSize: itemsRef.current.size, }), [setItemState, removeItem, hasAnyMatch, groupHasAnyMatch]) return ( {children} ) }) Command.displayName = "Command" const CommandDialog = ({ children, ...props }) => { const { open: openProp, defaultOpen, onOpenChange, ...rest } = props as any const [openState, setOpenState] = React.useState(defaultOpen || false) const open = openProp !== undefined ? openProp : openState const handleOpenChange = (newOpen: boolean) => { if (openProp === undefined) setOpenState(newOpen) onOpenChange?.(newOpen) } const enhancedChildren = React.useMemo(() => { const enhance = (node: React.ReactNode): React.ReactNode => React.Children.map(node, (child) => { if (!React.isValidElement(child)) return child if (child.type === CommandInput) { if (child.props?.focus === false) return child return React.cloneElement(child as any, { focus: open, className: cn(child.props?.className, "pr-11") }) } if (!child.props?.children) return child return React.cloneElement(child as any, undefined, enhance(child.props.children)) }) return enhance(children) }, [children, open]) return ( {enhancedChildren} ) } const CommandInput = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { focus?: boolean } >(({ className, placeholderClass, focus = true, ...props }, ref) => { const context = React.useContext(CommandContext) const [localValue, setLocalValue] = React.useState(context?.search ?? "") const lastSyncedSearchRef = React.useRef(context?.search ?? "") const [inputFocus, setInputFocus] = React.useState(false) const focusTimerRef = React.useRef | null>(null) React.useEffect(() => { // 只有当 context.search 与上次同步的值不同,且与当前输入值也不同时,才进行强制同步(通常是外部重置了搜索内容) if (context?.search !== lastSyncedSearchRef.current && context?.search !== localValue) { setLocalValue(context?.search ?? "") lastSyncedSearchRef.current = context?.search ?? "" } }, [context?.search, localValue]) React.useEffect(() => { if (focusTimerRef.current) clearTimeout(focusTimerRef.current) focusTimerRef.current = null if (!focus) { setInputFocus(false) return } setInputFocus(false) const schedule = () => { focusTimerRef.current = setTimeout(() => { setInputFocus(true) focusTimerRef.current = null }, 0) } if (typeof (Taro as any)?.nextTick === "function") { ;(Taro as any).nextTick(schedule) } else { schedule() } return () => { if (focusTimerRef.current) clearTimeout(focusTimerRef.current) focusTimerRef.current = null } }, [focus]) return ( { const v = e.detail.value setLocalValue(v) lastSyncedSearchRef.current = v context?.setSearch(v) }} focus={inputFocus} {...props} /> ) }) CommandInput.displayName = "CommandInput" const CommandList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandList.displayName = "CommandList" const CommandEmpty = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { const context = React.useContext(CommandItemsContext) const show = context ? context.itemsSize > 0 && !context.hasAnyMatch() : false if (!show) return null return ( ) }) CommandEmpty.displayName = "CommandEmpty" const CommandGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { heading?: React.ReactNode } >(({ className, heading, children, ...props }, ref) => { const context = React.useContext(CommandItemsContext) const groupId = React.useId() const show = !context || context.itemsSize === 0 || context.groupHasAnyMatch(groupId) return ( {heading && ( {heading} )} {children} ) }) CommandGroup.displayName = "CommandGroup" const CommandSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandSeparator.displayName = "CommandSeparator" const CommandItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { value?: string onSelect?: (value: string) => void disabled?: boolean } >(({ className, value, onSelect, disabled, children, ...props }, ref) => { const context = React.useContext(CommandContext) const itemsContext = React.useContext(CommandItemsContext) const group = React.useContext(GroupContext) const id = React.useId() const computedValue = React.useMemo(() => (value ?? getNodeText(children)).trim(), [value, children]) const search = (context?.deferredSearch ?? "").trim().toLowerCase() const match = React.useMemo(() => !search || (!!computedValue && computedValue.toLowerCase().includes(search)) , [search, computedValue]) React.useEffect(() => { if (!itemsContext) return itemsContext.setItemState(id, { match, groupId: group?.groupId }) return () => itemsContext.removeItem(id) }, [itemsContext, id, match, group?.groupId]) return ( !disabled && onSelect?.(computedValue)} {...props} > {children} ) }) CommandItem.displayName = "CommandItem" const CommandShortcut = ({ className, ...props }: React.ComponentPropsWithoutRef) => { return ( ) } CommandShortcut.displayName = "CommandShortcut" export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, }