SearchState

最近更新时间:2025-09-12 18:14:52

我的收藏

概述

SearchState 是基于 Vue 3 Composition API 的搜索状态管理钩子,为 Search 组件提供完整的状态管理能力。它支持多种搜索模式(标准模式、嵌入式模式),管理搜索关键词、搜索结果、加载状态、错误处理等功能。如果自定义组件能力不能支持您的业务,可以使用 SearchState 实现您的需求。

数据

属性名
类型
说明
keyword
Ref<string>
当前搜索关键词
results
Ref<Map<SearchType, SearchResult<SearchType>>>
搜索结果集合
isLoading
Ref<boolean>
是否正在搜索
error
Ref<Error | null>
搜索错误信息
searchAdvancedParams
Ref<Map<SearchType, SearchParamsMap[SearchType]>>
高级搜索参数
selectedSearchType
Ref<SearchType | 'all'>
当前选中的搜索类型

方法

方法名
类型
说明
setKeyword
(k: string) => Promise<void>
设置搜索关键词
loadMore
(type?: SearchType) => Promise<void>
加载更多搜索结果
setSelectedType
(type: SearchType | 'all') => void
设置搜索类型
setSearchMessageAdvancedParams
(params: SearchCloudMessagesParams) => void
设置消息搜索高级参数
setSearchUserAdvancedParams
(params: SearchCloudUsersParams) => void
设置用户搜索高级参数
setSearchGroupAdvancedParams
(params: SearchCloudGroupsParams) => void
设置群组搜索高级参数

使用示例

<template>
<div class="custom-search-app">
<div class="search-header">
<input
v-model="searchInput"
type="text"
placeholder="输入搜索关键词..."
@keyup.enter="performSearch"
/>
<button @click="performSearch" :disabled="isLoading">
{{ isLoading ? '搜索中...' : '搜索' }}
</button>
</div>

<div class="search-tabs">
<button
v-for="tab in searchTabs"
:key="tab.type"
:class="['tab-btn', { active: selectedSearchType === tab.type }]"
@click="setSelectedType(tab.type)"
>
{{ tab.label }}
<span v-if="getResultCount(tab.type) > 0" class="result-count">
{{ getResultCount(tab.type) }}
</span>
</button>
</div>

<div class="search-content">
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<div>搜索中...</div>
</div>

<div v-else-if="error" class="error-state">
<div class="error-icon"></div>
<div>搜索失败: {{ error.message }}</div>
<button @click="retrySearch">重试</button>
</div>

<div v-else-if="keyword && results.size === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<div>未找到相关结果</div>
</div>

<div v-else-if="keyword" class="results-container">
<div
v-for="[type, result] in filteredResults"
:key="type"
class="result-section"
>
<h3>{{ getTypeLabel(type) }} ({{ result.resultList.length }})</h3>
<div
v-for="(item, index) in result.resultList"
:key="index"
class="result-item"
@click="handleItemClick(item, type)"
>
<component
:is="getResultItemComponent(type)"
:data="item"
:keyword="keyword"
:type="type"
/>
</div>

<button
v-if="result.hasMore"
@click="loadMore(type)"
class="load-more-btn"
>
加载更多
</button>
</div>
</div>

<div v-else class="presearch-state">
<div class="presearch-icon">🔍</div>
<div>输入关键词开始搜索</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useSearchState, VariantType, SearchType } from '@tencentcloud/chat-uikit-vue3';
import type { SearchResultItemType } from '@tencentcloud/chat-uikit-vue3';

const {
keyword,
results,
isLoading,
error,
selectedSearchType,
setKeyword,
setSelectedType,
loadMore
} = useSearchState(VariantType.STANDARD);

const searchInput = ref('');

const searchTabs = [
{ type: 'all', label: '全部' },
{ type: SearchType.USER, label: '用户' },
{ type: SearchType.GROUP, label: '群组' },
{ type: SearchType.MESSAGE, label: '消息' }
];

const filteredResults = computed(() => {
if (selectedSearchType.value === 'all') {
return Array.from(results.value.entries());
} else {
const result = results.value.get(selectedSearchType.value);
return result ? [[selectedSearchType.value, result]] : [];
}
});

const getResultCount = (type: string) => {
if (type === 'all') {
return Array.from(results.value.values())
.reduce((total, result) => total + result.resultList.length, 0);
}
return results.value.get(type as SearchType)?.resultList.length || 0;
};

const getTypeLabel = (type: SearchType) => {
const labels = {
[SearchType.USER]: '用户',
[SearchType.GROUP]: '群组',
[SearchType.MESSAGE]: '消息',
[SearchType.CHAT_MESSAGE]: '聊天消息'
};
return labels[type] || type;
};

const getResultItemComponent = (type: SearchType) => {
// 返回对应类型的结果项组件
return 'div'; // 简化示例
};

const performSearch = async () => {
if (!searchInput.value.trim()) return;

try {
await setKeyword(searchInput.value);
} catch (err) {
console.error('搜索失败:', err);
}
};

const retrySearch = () => {
performSearch();
};

const handleItemClick = (item: SearchResultItemType, type: SearchType) => {
console.log('点击结果:', item, type);
};
</script>

<style scoped>
.custom-search-app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

.search-header {
display: flex;
gap: 12px;
margin-bottom: 20px;
}

.search-header input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
}

.search-header button {
padding: 10px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}

.search-header button:disabled {
background: #d9d9d9;
cursor: not-allowed;
}

.search-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
}

.tab-btn {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}

.tab-btn:hover {
border-color: #1890ff;
color: #1890ff;
}

.tab-btn.active {
background: #1890ff;
border-color: #1890ff;
color: white;
}

.result-count {
background: rgba(255, 255, 255, 0.2);
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}

.search-content {
min-height: 400px;
}

.loading-state,
.error-state,
.empty-state,
.presearch-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
color: #666;
}

.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}

.error-icon,
.empty-icon,
.presearch-icon {
font-size: 48px;
margin-bottom: 16px;
}

.result-section {
margin-bottom: 24px;
}

.result-section h3 {
color: #333;
margin-bottom: 12px;
font-size: 16px;
}

.result-item {
border-bottom: 1px solid #f0f0f0;
}

.load-more-btn {
width: 100%;
padding: 12px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #666;
margin-top: 8px;
transition: all 0.2s;
}

.load-more-btn:hover {
background: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
接入组件后效果如下:
搜索页示例
搜索结果示例