# Search 组件自定义文档

## 概述

Search 是一个功能完整的搜索组件，支持用户、群组、消息的搜索功能。它集成了搜索框、高级搜索、搜索结果展示等功能，支持三种显示模式（迷你、标准、嵌入式）和高度自定义。组件内置防抖搜索、分页加载、错误处理等功能，适用于各种搜索场景。

## Props 配置表格

| 字段 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| variant | `VariantType` | `VariantType.MINI` | 搜索模式：`mini`(迷你)、`standard`(标准)、`embedded`(嵌入式) |
| SearchBar | `React.ComponentType<SearchBarProps>` | `DefaultSearchBar` | 自定义搜索框组件 |
| SearchResults | `React.ComponentType<SearchResultsProps>` | `DefaultSearchResults` | 自定义搜索结果组件 |
| SearchAdvanced | `React.ComponentType<SearchAdvancedProps>` | `DefaultSearchAdvanced` | 自定义高级搜索组件 |
| SearchResultsPresearch | `React.ComponentType` | - | 搜索前占位符组件 |
| SearchResultsLoading | `React.ComponentType` | - | 加载中占位符组件 |
| SearchResultsEmpty | `React.ComponentType` | - | 空结果占位符组件 |
| SearchResultItem | `React.ComponentType<ResultItemProps<SearchType>>` | - | 自定义搜索结果项组件 |
| debounceTime | `number` | `300` | 搜索防抖时间(毫秒) |
| autoFocus | `boolean` | `false` | 是否自动聚焦搜索框 |
| className | `string` | - | 自定义样式类名 |
| style | `React.CSSProperties` | - | 自定义样式 |
| onKeywordChange | `(keyword: string) => void` | - | 搜索关键词变化回调 |
| onSearchComplete | `(results: Map<SearchType, ISearchResult<SearchType>>) => void` | - | 搜索完成回调 |
| onResultItemClick | `(data: SearchResultItem, type: SearchType) => void` | - | 搜索结果点击回调 |
| onError | `(error: Error) => void` | - | 搜索错误回调 |

## Props 详解

### variant

type: `VariantType`

variant 用于设置搜索组件的显示模式，默认值为 `VariantType.MINI`。

#### Example1: 迷你模式搜索

```tsx
import { Search, VariantType } from 'tuikit-atomicx-react';

function MiniSearch() {
  return (
    <Search
      variant={VariantType.MINI}
      onResultItemClick={(data, type) => {
        console.log('点击搜索结果:', data, type);
      }}
    />
  );
}
```

#### Example2: 标准模式搜索

```tsx
import { Search, VariantType } from 'tuikit-atomicx-react';

function StandardSearch() {
  return (
    <Search
      variant={VariantType.STANDARD}
      autoFocus={true}
      onKeywordChange={(keyword) => {
        console.log('搜索关键词变化:', keyword);
      }}
      onResultItemClick={(data, type) => {
        // 处理搜索结果点击
        if (type === 'USER') {
          // 跳转到用户详情页
          navigateToUserProfile(data.userID);
        } else if (type === 'GROUP') {
          // 加入群组
          joinGroup(data.groupID);
        }
      }}
    />
  );
}
```

#### Example3: 嵌入式消息搜索

```tsx
import { Search, VariantType } from 'tuikit-atomicx-react';

function EmbeddedMessageSearch({ conversationID }: { conversationID: string }) {
  return (
    <Search
      variant={VariantType.EMBEDDED}
      onResultItemClick={(data, type) => {
        if (type === 'MESSAGE') {
          // 定位到具体消息
          scrollToMessage(data.messageID);
        }
      }}
    />
  );
}
```

### SearchBar

type: `React.ComponentType<SearchBarProps>`

SearchBar 用于自定义搜索框组件，默认值为 `DefaultSearchBar`。

#### Example1: 自定义搜索框样式

```tsx
import { Search, SearchBarProps } from 'tuikit-atomicx-react';
import { Icon, IconSearch } from '@tencentcloud/uikit-base-component-react';

const CustomSearchBar = ({ value, onChange, onClear, placeholder }: SearchBarProps) => (
  <div className="custom-search-bar">
    <Icon><IconSearch /></Icon>
    <input
      type="text"
      value={value}
      onChange={onChange}
      placeholder={placeholder || "请输入搜索内容..."}
      className="custom-input"
    />
    {value && (
      <button onClick={onClear} className="clear-btn">
        清除
      </button>
    )}
  </div>
);

function SearchWithCustomBar() {
  return (
    <Search
      SearchBar={CustomSearchBar}
      onKeywordChange={(keyword) => {
        console.log('搜索:', keyword);
      }}
    />
  );
}
```

#### Example2: 带搜索建议的搜索框

```tsx
import { Search, SearchBarProps } from 'tuikit-atomicx-react';
import { useState } from 'react';

const SearchBarWithSuggestions = ({ value, onChange, onKeyDown, ...props }: SearchBarProps) => {
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [suggestions] = useState(['用户名搜索', '群组名搜索', '消息内容搜索']);

  return (
    <div className="search-bar-with-suggestions">
      <input
        type="text"
        value={value}
        onChange={(e) => {
          onChange?.(e);
          setShowSuggestions(e.target.value.length === 0);
        }}
        onKeyDown={onKeyDown}
        onFocus={() => setShowSuggestions(value?.length === 0)}
        onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
        {...props}
      />
      {showSuggestions && (
        <div className="suggestions-dropdown">
          {suggestions.map((suggestion, index) => (
            <div key={index} className="suggestion-item">
              {suggestion}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

function SearchWithSuggestions() {
  return <Search SearchBar={SearchBarWithSuggestions} />;
}
```

### SearchResults

type: `React.ComponentType<SearchResultsProps>`

SearchResults 用于自定义搜索结果展示组件，默认值为 `DefaultSearchResults`。

#### Example1: 自定义搜索结果布局

```tsx
import { Search, SearchResultsProps, ISearchType } from 'tuikit-atomicx-react';

const CustomSearchResults = ({ 
  results, 
  keyword, 
  onResultItemClick, 
  isLoading 
}: SearchResultsProps) => (
  <div className="custom-search-results">
    {isLoading && <div className="loading">搜索中...</div>}
    
    {Array.from(results.entries()).map(([type, result]) => (
      <div key={type} className="result-section">
        <h3 className="section-title">
          {type === ISearchType.USER ? '用户' : 
           type === ISearchType.GROUP ? '群组' : '消息'}
          ({result.totalCount})
        </h3>
        
        <div className="result-grid">
          {result.resultList.map((item, index) => (
            <div 
              key={index} 
              className="result-card"
              onClick={() => onResultItemClick?.(item, type)}
            >
              {/* 自定义结果项展示 */}
              <div className="result-content">
                <h4>{item.name || item.groupName || '消息'}</h4>
                <p>{item.description || item.lastMessage}</p>
              </div>
            </div>
          ))}
        </div>
      </div>
    ))}
  </div>
);

function SearchWithCustomResults() {
  return (
    <Search
      SearchResults={CustomSearchResults}
      onResultItemClick={(data, type) => {
        console.log('点击结果:', data, type);
      }}
    />
  );
}
```

### SearchResultItem

type: `React.ComponentType<ResultItemProps<ISearchType>>`

SearchResultItem 用于自定义单个搜索结果项的展示，默认值为内置的结果项组件。

#### Example1: 自定义用户搜索结果项

```tsx
import { Search, ResultItemProps, ISearchType } from 'tuikit-atomicx-react';
import { Avatar } from 'tuikit-atomicx-react';

const CustomUserResultItem = ({ 
  data, 
  type, 
  keyword, 
  onClick 
}: ResultItemProps<SearchType.USER>) => (
  <div 
    className="custom-user-item"
    onClick={() => onClick?.(data, type)}
  >
    <Avatar src={data.avatar} size="40px" />
    <div className="user-info">
      <div className="user-name">
        {/* 高亮显示匹配的关键词 */}
        {data.nick?.replace(
          new RegExp(keyword, 'gi'), 
          `<mark>$&</mark>`
        )}
      </div>
      <div className="user-id">ID: {data.userID}</div>
      {data.selfSignature && (
        <div className="user-signature">{data.selfSignature}</div>
      )}
    </div>
    <div className="user-actions">
      <button className="add-friend-btn">添加好友</button>
    </div>
  </div>
);

function SearchWithCustomUserItem() {
  return (
    <Search
      SearchResultItem={CustomUserResultItem}
      onResultItemClick={(data, type) => {
        if (type === SearchType.USER) {
          // 处理用户点击逻辑
          showUserProfile(data.userID);
        }
      }}
    />
  );
}
```

#### Example2: 自定义消息搜索结果项

```tsx
import { Search, ResultItemProps, SearchType } from 'tuikit-atomicx-react';

const CustomMessageResultItem = ({ 
  data, 
  keyword, 
  onClick 
}: ResultItemProps<SearchType.MESSAGE>) => (
  <div 
    className="custom-message-item"
    onClick={() => onClick?.(data, SearchType.MESSAGE)}
  >
    <div className="message-header">
      <span className="conversation-name">
        {data.conversation?.getShowName()}
      </span>
      <span className="message-time">
        {new Date(data.lastTime * 1000).toLocaleString()}
      </span>
    </div>
    
    <div className="message-content">
      {/* 高亮显示匹配内容 */}
      <div dangerouslySetInnerHTML={{
        __html: data.messageForShow?.replace(
          new RegExp(keyword, 'gi'),
          `<mark class="highlight">$&</mark>`
        )
      }} />
    </div>
    
    <div className="message-meta">
      <span className="sender">发送者: {data.from}</span>
      <span className="message-type">
        {data.type === 'TIMTextElem' ? '文本' : 
         data.type === 'TIMImageElem' ? '图片' : '其他'}
      </span>
    </div>
  </div>
);

function SearchWithCustomMessageItem() {
  return (
    <Search
      variant="standard"
      SearchResultItem={CustomMessageResultItem}
      onResultItemClick={(data, type) => {
        if (type === SearchType.MESSAGE) {
          // 跳转到具体消息位置
          navigateToMessage(data.conversationID, data.messageID);
        }
      }}
    />
  );
}
```

### SearchResultsPresearch

type: `React.ComponentType`

SearchResultsPresearch 用于自定义搜索前的占位符组件，在用户未输入搜索关键词时显示。

#### Example1: 搜索建议占位符

```tsx
import { Search } from 'tuikit-atomicx-react';

const SearchSuggestions = () => (
  <div className="search-suggestions">
    <h3>搜索建议</h3>
    <div className="suggestion-categories">
      <div className="category">
        <h4>用户搜索</h4>
        <ul>
          <li>输入用户名或用户ID</li>
          <li>支持模糊匹配</li>
        </ul>
      </div>
      <div className="category">
        <h4>群组搜索</h4>
        <ul>
          <li>输入群组名称</li>
          <li>输入群组ID</li>
        </ul>
      </div>
      <div className="category">
        <h4>消息搜索</h4>
        <ul>
          <li>输入消息关键词</li>
          <li>支持时间范围筛选</li>
        </ul>
      </div>
    </div>
  </div>
);

function SearchWithSuggestions() {
  return (
    <Search
      SearchResultsPresearch={SearchSuggestions}
      onKeywordChange={(keyword) => {
        console.log('开始搜索:', keyword);
      }}
    />
  );
}
```

#### Example2: 搜索历史占位符

```tsx
import { Search } from 'tuikit-atomicx-react';
import { useState, useEffect } from 'react';

const SearchHistory = () => {
  const [history, setHistory] = useState<string[]>([]);

  useEffect(() => {
    // 从本地存储加载搜索历史
    const savedHistory = localStorage.getItem('searchHistory');
    if (savedHistory) {
      setHistory(JSON.parse(savedHistory));
    }
  }, []);

  const handleHistoryClick = (keyword: string) => {
    // 触发搜索
    console.log('点击历史搜索:', keyword);
  };

  return (
    <div className="search-history">
      <h3>搜索历史</h3>
      {history.length > 0 ? (
        <div className="history-list">
          {history.map((keyword, index) => (
            <div 
              key={index}
              className="history-item"
              onClick={() => handleHistoryClick(keyword)}
            >
              {keyword}
            </div>
          ))}
        </div>
      ) : (
        <div className="no-history">暂无搜索历史</div>
      )}
    </div>
  );
};

function SearchWithHistory() {
  return (
    <Search
      SearchResultsPresearch={SearchHistory}
      onKeywordChange={(keyword) => {
        // 保存搜索历史
        if (keyword.trim()) {
          const history = JSON.parse(localStorage.getItem('searchHistory') || '[]');
          const newHistory = [keyword, ...history.filter(item => item !== keyword)].slice(0, 10);
          localStorage.setItem('searchHistory', JSON.stringify(newHistory));
        }
      }}
    />
  );
}
```

### SearchResultsLoading

type: `React.ComponentType`

SearchResultsLoading 用于自定义搜索加载中的占位符组件。

#### Example1: 自定义加载动画

```tsx
import { Search } from 'tuikit-atomicx-react';

const CustomLoading = () => (
  <div className="custom-loading">
    <div className="loading-spinner">
      <div className="spinner-circle"></div>
    </div>
    <div className="loading-text">
      <p>正在搜索中...</p>
      <p className="loading-tip">搜索范围：用户、群组、消息</p>
    </div>
  </div>
);

function SearchWithCustomLoading() {
  return (
    <Search
      SearchResultsLoading={CustomLoading}
      debounceTime={500} // 增加防抖时间
      onError={(error) => {
        console.error('搜索出错:', error);
      }}
    />
  );
}
```

### SearchResultsEmpty

type: `React.ComponentType`

SearchResultsEmpty 用于自定义搜索无结果时的占位符组件。

#### Example1: 自定义空结果提示

```tsx
import { Search } from 'tuikit-atomicx-react';

const CustomEmptyResult = () => (
  <div className="custom-empty-result">
    <div className="empty-icon">🔍</div>
    <h3>未找到相关结果</h3>
    <div className="empty-suggestions">
      <p>建议您：</p>
      <ul>
        <li>检查输入的关键词是否正确</li>
        <li>尝试使用其他关键词</li>
        <li>使用更短的关键词进行搜索</li>
      </ul>
    </div>
    <div className="empty-actions">
      <button className="retry-btn">重新搜索</button>
      <button className="feedback-btn">意见反馈</button>
    </div>
  </div>
);

function SearchWithCustomEmpty() {
  return (
    <Search
      SearchResultsEmpty={CustomEmptyResult}
      onResultItemClick={(data, type) => {
        console.log('点击结果:', data, type);
      }}
    />
  );
}
```

### debounceTime

type: `number`

debounceTime 用于设置搜索防抖时间（毫秒），默认值为 `300`。

#### Example1: 快速响应搜索

```tsx
import { Search } from 'tuikit-atomicx-react';

function FastSearch() {
  return (
    <Search
      debounceTime={100} // 100ms 快速响应
      onKeywordChange={(keyword) => {
        console.log('快速搜索:', keyword);
      }}
    />
  );
}
```

#### Example2: 节流搜索请求

```tsx
import { Search } from 'tuikit-atomicx-react';

function ThrottledSearch() {
  return (
    <Search
      debounceTime={800} // 800ms 减少请求频率
      onKeywordChange={(keyword) => {
        console.log('节流搜索:', keyword);
        // 适用于网络较慢或需要减少服务器压力的场景
      }}
    />
  );
}
```

### onKeywordChange

type: `(keyword: string) => void`

onKeywordChange 用于监听搜索关键词变化的回调函数。

#### Example1: 搜索关键词统计

```tsx
import { Search } from 'tuikit-atomicx-react';
import { useState } from 'react';

function SearchWithAnalytics() {
  const [searchCount, setSearchCount] = useState(0);

  return (
    <div>
      <div className="search-stats">
        搜索次数: {searchCount}
      </div>
      <Search
        onKeywordChange={(keyword) => {
          if (keyword.trim()) {
            setSearchCount(prev => prev + 1);
            
            // 发送搜索统计
            analytics.track('search_keyword', {
              keyword,
              timestamp: Date.now(),
              length: keyword.length
            });
          }
        }}
      />
    </div>
  );
}
```

#### Example2: 搜索建议联动

```tsx
import { Search } from 'tuikit-atomicx-react';
import { useState, useEffect } from 'react';

function SearchWithSuggestions() {
  const [suggestions, setSuggestions] = useState<string[]>([]);

  const fetchSuggestions = async (keyword: string) => {
    if (keyword.length < 2) {
      setSuggestions([]);
      return;
    }
    
    try {
      const response = await fetch(`/api/search/suggestions?q=${encodeURIComponent(keyword)}`);
      const data = await response.json();
      setSuggestions(data.suggestions || []);
    } catch (error) {
      console.error('获取搜索建议失败:', error);
    }
  };

  return (
    <div>
      <Search
        onKeywordChange={(keyword) => {
          fetchSuggestions(keyword);
        }}
      />
      
      {suggestions.length > 0 && (
        <div className="search-suggestions">
          {suggestions.map((suggestion, index) => (
            <div key={index} className="suggestion-item">
              {suggestion}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
```

### onResultItemClick

type: `(data: ISearchResultItem, type: ISearchType) => void`

onResultItemClick 用于处理搜索结果项点击的回调函数。

#### Example1: 多类型结果处理

```tsx
import { Search, SearchType } from 'tuikit-atomicx-react';
import { useNavigate } from 'react-router-dom';

function SearchWithNavigation() {
  const navigate = useNavigate();

  const handleResultClick = (data: any, type: SearchType) => {
    switch (type) {
      case SearchType.USER:
        // 跳转到用户详情页
        navigate(`/user/${data.userID}`);
        break;
        
      case SearchType.GROUP:
        // 跳转到群组详情页
        navigate(`/group/${data.groupID}`);
        break;
        
      case SearchType.MESSAGE:
        // 跳转到对话并定位消息
        navigate(`/chat/${data.conversation.conversationID}`, {
          state: { messageID: data.messageID }
        });
        break;
        
      default:
        console.log('未知类型:', type, data);
    }
  };

  return (
    <Search
      variant="standard"
      onResultItemClick={handleResultClick}
    />
  );
}
```

#### Example2: 结果点击统计和权限控制

```tsx
import { Search, SearchType } from 'tuikit-atomicx-react';
import { useState } from 'react';

function SearchWithPermissionControl() {
  const [clickHistory, setClickHistory] = useState<any[]>([]);

  const handleResultClick = async (data: any, type: SearchType) => {
    try {
      // 记录点击统计
      const clickRecord = {
        type,
        data: data.userID || data.groupID || data.messageID,
        timestamp: Date.now()
      };
      setClickHistory(prev => [...prev, clickRecord]);

      // 权限检查
      const hasPermission = await checkPermission(type, data);
      if (!hasPermission) {
        alert('您没有权限访问此内容');
        return;
      }

      // 执行相应操作
      switch (type) {
        case SearchType.USER:
          await addToRecentContacts(data.userID);
          showUserProfile(data);
          break;
          
        case SearchType.GROUP:
          const canJoin = await checkGroupJoinPermission(data.groupID);
          if (canJoin) {
            showJoinGroupDialog(data);
          } else {
            showGroupInfo(data);
          }
          break;
          
        case SearchType.MESSAGE:
          await markMessageAsViewed(data.messageID);
          scrollToMessage(data);
          break;
      }
      
    } catch (error) {
      console.error('处理点击事件失败:', error);
      alert('操作失败，请重试');
    }
  };

  return (
    <div>
      <Search
        onResultItemClick={handleResultClick}
      />
      
      {/* 点击历史统计 */}
      <div className="click-stats">
        <h4>点击统计</h4>
        <p>用户: {clickHistory.filter(item => item.type === SearchType.USER).length}</p>
        <p>群组: {clickHistory.filter(item => item.type === SearchType.GROUP).length}</p>
        <p>消息: {clickHistory.filter(item => item.type === SearchType.MESSAGE).length}</p>
      </div>
    </div>
  );
}
```

### onError

type: `(error: Error) => void`

onError 用于处理搜索过程中发生的错误。

#### Example1: 错误提示和重试机制

```tsx
import { Search } from 'tuikit-atomicx-react';
import { useState } from 'react';

function SearchWithErrorHandling() {
  const [error, setError] = useState<string | null>(null);
  const [retryCount, setRetryCount] = useState(0);

  const handleError = (error: Error) => {
    console.error('搜索错误:', error);
    
    // 根据错误类型显示不同提示
    if (error.message.includes('network')) {
      setError('网络连接失败，请检查网络设置');
    } else if (error.message.includes('timeout')) {
      setError('搜索超时，请重试');
    } else if (error.message.includes('permission')) {
      setError('没有搜索权限，请联系管理员');
    } else {
      setError('搜索失败，请稍后重试');
    }
    
    // 记录错误统计
    analytics.track('search_error', {
      error: error.message,
      retryCount,
      timestamp: Date.now()
    });
  };

  const handleRetry = () => {
    setError(null);
    setRetryCount(prev => prev + 1);
    // 重新触发搜索
    window.location.reload();
  };

  return (
    <div>
      {error && (
        <div className="error-banner">
          <div className="error-message">{error}</div>
          <button onClick={handleRetry} className="retry-btn">
            重试 ({retryCount > 0 ? `第${retryCount}次` : ''})
          </button>
        </div>
      )}
      
      <Search
        onError={handleError}
        debounceTime={500} // 增加防抖时间减少错误概率
      />
    </div>
  );
}
```

#### Example2: 错误日志和用户反馈

```tsx
import { Search } from 'tuikit-atomicx-react';
import { useState } from 'react';

function SearchWithErrorLogging() {
  const [errorLogs, setErrorLogs] = useState<any[]>([]);
  const [showFeedback, setShowFeedback] = useState(false);

  const handleError = async (error: Error) => {
    const errorLog = {
      id: Date.now(),
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href
    };

    // 添加到本地错误日志
    setErrorLogs(prev => [...prev, errorLog]);

    // 发送错误日志到服务器
    try {
      await fetch('/api/error-logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorLog)
      });
    } catch (logError) {
      console.error('发送错误日志失败:', logError);
    }

    // 严重错误时显示反馈表单
    if (error.message.includes('critical') || error.message.includes('fatal')) {
      setShowFeedback(true);
    }
  };

  const submitFeedback = async (feedback: string) => {
    try {
      await fetch('/api/feedback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          feedback,
          errorLogs: errorLogs.slice(-3), // 最近3个错误
          timestamp: new Date().toISOString()
        })
      });
      
      alert('反馈已提交，感谢您的反馈！');
      setShowFeedback(false);
    } catch (error) {
      alert('提交反馈失败，请稍后重试');
    }
  };

  return (
    <div>
      <Search onError={handleError} />
      
      {/* 错误反馈弹窗 */}
      {showFeedback && (
        <div className="feedback-modal">
          <div className="feedback-content">
            <h3>遇到问题了？</h3>
            <p>我们检测到搜索功能出现了问题，请描述您遇到的情况：</p>
            <textarea
              placeholder="请详细描述您遇到的问题..."
              onBlur={(e) => {
                if (e.target.value.trim()) {
                  submitFeedback(e.target.value);
                }
              }}
            />
            <div className="feedback-actions">
              <button onClick={() => setShowFeedback(false)}>取消</button>
              <button onClick={() => {
                const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
                submitFeedback(textarea.value);
              }}>
                提交反馈
              </button>
            </div>
          </div>
        </div>
      )}
      
      {/* 开发环境下显示错误日志 */}
      {process.env.NODE_ENV === 'development' && errorLogs.length > 0 && (
        <div className="error-logs">
          <h4>错误日志 ({errorLogs.length})</h4>
          {errorLogs.slice(-5).map(log => (
            <div key={log.id} className="error-log-item">
              <div className="error-time">{log.timestamp}</div>
              <div className="error-message">{log.message}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
``` 