kb-search
v2.0.1
Published
A knowledge base search component library based on Vue 3 and Arco Design
Downloads
18
Maintainers
Readme
KbSearch Component Library
一个基于 Vue 3 + TypeScript + Arco Design 的知识库搜索组件库,支持普通、高级、智能三种搜索模式,开箱即用且高度可定制。
特性
- 🔍 三种搜索模式:普通搜索、高级搜索、智能搜索(AI 对话式)
- 📱 响应式设计:完美适配桌面端和移动端
- 🎨 主题定制:支持自定义主题色,通过 CSS Variables 暴露
- 🧩 纯 UI 组件:所有组件都是受控组件,不内置请求逻辑,灵活可控
- 📦 按需引入:支持独立组件单独引入,减少打包体积
- 🔌 事件驱动:所有操作通过 emit 事件通知父组件
- 💬 智能对话:支持流式响应、会话历史管理
- 🎯 推荐功能:支持热门关键词、推荐内容展示
安装
npm install kb-search依赖要求
- Vue 3.3.0+
- @arco-design/web-vue 2.50.0+
- dayjs (用于日期格式化)
注意:组件库不内置 axios 或 fetch,所有数据请求由业务方自行处理。
样式说明
✅ 无需手动导入样式:组件库已自动包含所有必要的样式,包括:
- Arco Design 的样式(自动导入)
- 组件库自己的样式(自动导入)
直接使用组件即可,无需额外导入任何样式文件。
快速开始
本组件库提供两种使用方式:
方式一:使用独立组件(推荐)
按需引入,打包体积更小:
<template>
<SimpleSearch
v-model:keyword="keyword"
:results="results"
:loading="loading"
@search="handleSearch"
/>
</template>
<script setup lang="ts">
import { SimpleSearch } from 'kb-search'
import { useSearch } from 'kb-search/hooks/useSearch'
const { keyword, results, loading, onSearch } = useSearch({
searchFn: async (k) => {
const res = await axios.get('/api/search', { params: { q: k } })
return { data: res.data.items, total: res.data.total }
}
})
</script>方式二:使用统一组件(组合组件)
支持多种模式切换:
<template>
<KbSearch
v-model:mode="mode"
v-model:simple-keyword="keyword"
:modes="modes"
:simple="simpleConfig"
:advanced="advancedConfig"
:smart="smartConfig"
@update:smartAskValues="handleUpdateSmartAskValues"
@simple-search="handleSimpleSearch"
@advanced-search="handleAdvancedSearch"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import axios from 'axios'
import { KbSearch } from 'kb-search'
import type {
SearchMode,
SearchParams,
SearchResultItem,
PaginationInfo,
RecommendChunk,
AdvancedSearchField,
ChatSession,
ChatMessage,
SmartAskField
} from 'kb-search'
// 模式相关
const mode = ref<SearchMode>('simple')
const modes = ref<SearchMode[]>(['simple', 'advanced', 'smart'])
// Simple 模式数据
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
// Advanced 模式数据
const fields = ref<AdvancedSearchField[]>([
{
key: 'title',
label: '标题',
type: 'text',
placeholder: '请输入标题关键词'
},
{
key: 'category',
label: '分类',
type: 'select',
options: [
{ label: '全部', value: '' },
{ label: '技术', value: 'tech' },
{ label: '商业', value: 'business' }
]
}
])
// Smart 模式数据
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const smartLoading = ref<boolean>(false)
const smartError = ref<string | null>(null)
const askFields = ref<SmartAskField[]>([])
const askValues = ref<Record<string, any>>({})
// 配置对象
const simpleConfig = computed(() => ({
keyword: keyword.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value,
recommend: recommend.value
}))
const advancedConfig = computed(() => ({
fields: fields.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const smartConfig = computed(() => ({
sessions: sessions.value,
currentSessionId: currentSessionId.value,
messages: messages.value,
loading: smartLoading.value,
error: smartError.value,
askFields: askFields.value,
askValues: askValues.value
}))
// 事件处理
const handleSimpleSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { keyword: params.keyword, ...params }
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleUpdateSmartAskValues = (values: Record<string, any>) => {
askValues.value = values
}
</script>组件架构
本组件库采用混合方案,提供两种使用方式:
1. 独立组件(纯 UI 组件)
三个独立的纯 UI 组件,可按需引入:
SimpleSearch- 普通搜索组件AdvancedSearch- 高级搜索组件SmartSearch- 智能搜索组件(AI 对话式)
特点:
- ✅ 纯 UI,不内置任何数据请求逻辑
- ✅ 所有数据、状态通过 props 传入
- ✅ 所有操作通过 emit 事件通知父组件
- ✅ 支持按需引入,减少打包体积
2. 统一组件(组合组件,props 分组命名)
KbSearch - 基于三个独立组件封装的组合组件,支持模式切换。
特点:
- ✅ 同样为纯 UI 组件
- ✅ 支持模式切换(simple/advanced/smart)
- ✅ 可配置启用哪些模式
- ✅ 开箱即用,适合需要多种模式的场景
使用建议
| 场景 | 推荐方案 | 说明 |
|------|---------|------|
| 只需要普通搜索 | SimpleSearch | 打包体积最小 |
| 只需要高级搜索 | AdvancedSearch | 按需引入 |
| 只需要智能搜索 | SmartSearch | 按需引入 |
| 需要多种模式切换 | KbSearch | 开箱即用 |
| 自定义组合布局 | 三个独立组件 | 完全自定义 |
独立组件使用
SimpleSearch(普通搜索)
定位说明
- ✅ 只画 UI,不做饭:不内置 axios、不内置请求、不内置 store
- ✅ 受控组件:所有数据、loading、错误、分页状态全部由父组件通过 props 传入
- ✅ 事件驱动:所有"搜索、翻页、选结果"动作只 emit 事件,不真干活
快速上手
<template>
<SimpleSearch
v-model:keyword="keyword"
:results="results"
:loading="loading"
@search="handleSearch"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SimpleSearch } from 'kb-search'
import { useSearch } from 'kb-search/hooks/useSearch'
import type { SearchResultItem } from 'kb-search'
const { keyword, results, loading, onSearch } = useSearch({
searchFn: async (k: string) => {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { q: k }
})
return { data: res.data.items, total: res.data.total }
}
})
const handleSearch = ({ keyword, extra }: { keyword: string; extra?: Record<string, any> }) => {
onSearch(keyword, extra)
}
</script>Props 列表(新增提问参数面板)
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| keyword | string | 否 | '' | 受控关键词,支持 v-model |
| loading | boolean | 是 | - | 搜索加载态 |
| error | string \| null | 否 | null | 错误提示文案 |
| results | SearchResultItem[] | 是 | [] | 搜索结果数组,最小字段 {id, title} |
| pagination | PaginationInfo | 否 | - | {current, pageSize, total, hasMore} |
| recommend | RecommendChunk[] | 否 | [] | 推荐块数组 |
| conditions | SearchCondition[] | 否 | [] | 搜索条件配置 |
| debounce | number | 否 | 300 | 防抖时长(ms) |
| placeholder | string | 否 | '输入关键词搜索...' | 输入框占位符 |
Events 列表
| 事件 | 参数 | 说明 |
|------|------|------|
| update:keyword | string | 输入框值变化 |
| search | {keyword: string, extra?: any} | 点击搜索或回车 |
| loadMore | - | 点击"加载更多" |
| select | SearchResultItem | 点击某条结果 |
Slots 列表
| 插槽名 | 用途 | 作用域参数 |
|--------|------|-----------|
| conditions | 条件控件区域 | { conditions } |
| recommend | 整个推荐区域 | { recommend } |
| recommend-item | 推荐块内单条项 | { chunk, items } |
| result | 单条结果卡片 | { item } |
使用示例
<template>
<SimpleSearch
v-model:keyword="keyword"
:loading="loading"
:error="error"
:results="results"
:pagination="pagination"
:recommend="recommend"
@search="handleSearch"
@load-more="handleLoadMore"
@select="handleSelect"
>
<!-- 自定义条件区域 -->
<template #conditions="{ conditions }">
<a-select v-model="selectedCategory" placeholder="选择分类">
<a-option value="all">全部</a-option>
<a-option value="doc">文档</a-option>
</a-select>
</template>
<!-- 自定义结果卡片 -->
<template #result="{ item }">
<CustomResultCard :item="item" />
</template>
</SimpleSearch>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SimpleSearch } from 'kb-search'
import type {
SearchResultItem,
RecommendChunk,
PaginationInfo,
SearchEventParams
} from 'kb-search'
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
const selectedCategory = ref<string>('all')
const handleSearch = async ({ keyword: kw, extra }: SearchEventParams) => {
loading.value = true
error.value = null
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { q: kw, ...extra }
})
results.value = res.data.items || []
pagination.value = {
current: 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleLoadMore = async () => {
if (!pagination.value.hasMore) return
const nextPage = (pagination.value.current || 1) + 1
loading.value = true
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: {
q: keyword.value,
page: nextPage,
pageSize: pagination.value.pageSize || 10
}
})
results.value = [...results.value, ...(res.data.items || [])]
pagination.value = {
...pagination.value,
current: nextPage,
hasMore: results.value.length < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '加载失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑,如跳转到详情页
}
</script>主题定制
组件使用 CSS Variables 暴露主题变量,业务方可以轻松覆盖:
.simple-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
--search-highlight-bg: #e8f3ff;
--search-highlight-color: #165DFF;
}AdvancedSearch(高级搜索)
定位说明
- ✅ 纯 UI 组件,不内置请求逻辑
- ✅ 表单字段完全由外部配置,支持动态字段配置
- ✅ 搜索结果格式与普通搜索一致,使用
SearchResultItem类型,可复用 - ✅ 支持多种字段类型:文本、下拉、多选、日期、日期范围、数字、开关等
- ✅ 多字段筛选表单
- ✅ 支持标题、内容、标签、日期范围等筛选
基本使用
<template>
<AdvancedSearch
:fields="fields"
:loading="loading"
:error="error"
:results="results"
:pagination="pagination"
@search="handleAdvancedSearch"
@load-more="handleLoadMore"
@select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { AdvancedSearch } from 'kb-search'
import type {
AdvancedSearchField,
SearchParams,
SearchResultItem,
PaginationInfo
} from 'kb-search'
// 定义表单字段配置
const fields = ref<AdvancedSearchField[]>([
{
key: 'title',
label: '标题',
type: 'text',
placeholder: '请输入标题关键词'
},
{
key: 'content',
label: '内容',
type: 'text',
placeholder: '请输入内容关键词'
},
{
key: 'category',
label: '分类',
type: 'select',
options: [
{ label: '全部', value: '' },
{ label: '技术', value: 'tech' },
{ label: '商业', value: 'business' }
]
},
{
key: 'tags',
label: '标签',
type: 'multi-select',
options: [
{ label: 'Vue', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'TypeScript', value: 'ts' }
]
},
{
key: 'dateRange',
label: '日期范围',
type: 'date-range',
placeholder: ['开始日期', '结束日期']
},
{
key: 'fuzzy',
label: '模糊检索',
type: 'switch',
defaultValue: true
}
])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
// 处理高级搜索(结果格式与普通搜索一致)
const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
// 调用后端 API,filters 包含所有表单字段的值
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
})
// 返回结果格式与普通搜索一致,使用 SearchResultItem
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleLoadMore = async () => {
if (!pagination.value.hasMore) return
const nextPage = (pagination.value.current || 1) + 1
loading.value = true
try {
// 获取当前搜索条件(需要根据实际情况保存)
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
page: nextPage,
pageSize: pagination.value.pageSize || 10
})
results.value = [...results.value, ...(res.data.items || [])]
pagination.value = {
...pagination.value,
current: nextPage,
hasMore: results.value.length < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '加载失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑
}
</script>Props 列表
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| fields | AdvancedSearchField[] | 是 | - | 表单字段配置列表(见下方字段配置说明) |
| loading | boolean | 否 | false | 加载状态 |
| error | string \| null | 否 | null | 错误信息 |
| formLayout | { labelCol?, wrapperCol?, labelAlign? } | 否 | { labelCol: 3, wrapperCol: 18, labelAlign: 'right' } | 表单布局配置 |
字段配置说明(AdvancedSearchField)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| key | string | 是 | 字段唯一标识(对应后端字段名) |
| label | string | 是 | 字段标签(显示名称) |
| type | AdvancedFieldType | 是 | 字段类型(见下方支持的类型) |
| placeholder | string \| [string, string] | 否 | 占位符(date-range 类型支持数组) |
| options | Array<{label: string, value: any}> | 否 | 选项列表(select、multi-select、radio 类型必需) |
| defaultValue | any | 否 | 默认值 |
| required | boolean | 否 | 是否必填 |
| disabled | boolean | 否 | 是否禁用 |
| props | Record<string, any> | 否 | 额外的组件属性(传递给具体的表单控件) |
支持的字段类型(AdvancedFieldType):
text- 文本输入框select- 单选下拉框(需要 options)multi-select- 多选下拉框(需要 options)date- 单个日期选择器date-range- 日期范围选择器number- 数字输入框switch- 开关(布尔值)checkbox- 复选框(布尔值)radio- 单选按钮组(需要 options)
Events 列表
| 事件 | 参数 | 说明 |
|------|------|------|
| search | SearchParams | 提交搜索表单时触发,params.filters 包含所有表单字段的值 |
重要说明:
- 搜索结果格式与普通搜索完全一致,使用
SearchResultItem类型 - 高级搜索和普通搜索的结果可以完全复用同一套渲染逻辑
params.filters是一个对象,key 为字段的key,value 为表单输入的值
SmartSearch(智能搜索)
定位说明
- ✅ 纯 UI 组件,不内置请求逻辑
- ✅ AI 对话式搜索界面
- ✅ 支持会话历史管理(由业务方管理)
- ✅ 支持流式响应显示
基本使用
<template>
<SmartSearch
:sessions="sessions"
:current-session-id="currentSessionId"
:messages="messages"
:loading="loading"
:error="error"
:ask-fields="askFields"
:ask-values="askValues"
@send="handleSend"
@new-session="handleNewSession"
@switch-session="handleSwitchSession"
@clear-session="handleClearSession"
@stop="handleStop"
@update:askValues="handleUpdateAskValues"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SmartSearch } from 'kb-search'
import type {
ChatSession,
ChatMessage,
SmartAskField
} from 'kb-search'
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const askFields = ref<SmartAskField[]>([])
const askValues = ref<Record<string, any>>({})
const handleSend = async (message: string) => {
loading.value = true
error.value = null
try {
// 添加用户消息
const userMsg: ChatMessage = {
id: Date.now(),
role: 'user',
content: message
}
messages.value.push(userMsg)
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
// 调用 API(支持流式响应)
const res = await axios.post<{ content: string }>('/api/ai/chat', {
message,
params: askValues.value
})
aiMsg.content = res.data.content
} catch (err: any) {
error.value = err.message || '发送失败'
// 移除失败的 AI 消息占位
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
} finally {
loading.value = false
}
}
const handleNewSession = () => {
const newSession: ChatSession = {
id: Date.now(),
title: '新会话',
createdAt: new Date()
}
sessions.value.push(newSession)
currentSessionId.value = newSession.id
messages.value = []
}
const handleSwitchSession = (sessionId: string | number) => {
currentSessionId.value = sessionId
// 根据 sessionId 加载对应的消息(需要根据实际情况实现)
// messages.value = loadMessagesBySessionId(sessionId)
}
const handleClearSession = (sessionId: string | number) => {
messages.value = []
// 可选:从 sessions 中移除该会话
const index = sessions.value.findIndex(s => s.id === sessionId)
if (index > -1) {
sessions.value.splice(index, 1)
}
if (currentSessionId.value === sessionId) {
currentSessionId.value = ''
}
}
const handleStop = () => {
loading.value = false
// 停止流式响应(需要根据实际情况实现)
}
const handleUpdateAskValues = (values: Record<string, any>) => {
askValues.value = values
}
</script>Props 列表
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| sessions | ChatSession[] | 否 | [] | 会话列表 |
| currentSessionId | string \| number | 否 | - | 当前会话ID |
| messages | ChatMessage[] | 否 | [] | 当前会话的消息列表 |
| loading | boolean | 否 | false | 加载状态 |
| error | string \| null | 否 | null | 错误信息 |
| showSidebar | boolean | 否 | true | 是否显示侧边栏 |
| askFields | SmartAskField[] | 否 | [] | 右侧提问参数字段配置(动态渲染) |
| askValues | Record<string, any> | 否 | - | 右侧提问参数受控值(配合 update:askValues) |
Events 列表(新增)
| 事件 | 参数 | 说明 |
|------|------|------|
| send | string | 发送消息 |
| send-with | { message: string, params: Record<string, any> } | 携带右侧参数发送 |
| update:askValues | Record<string, any> | 右侧表单值受控同步 |
| new-session | - | 新建会话 |
| switch-session | string \| number | 切换会话 |
| clear-session | string \| number | 清空会话 |
| stop | - | 停止生成 |
| update-session-title | sessionId, title | 更新会话标题 |
统一组件 KbSearch
KbSearch 是一个组合组件,基于三个独立组件封装,支持模式切换。同样为纯 UI 组件,所有数据通过 props 传入。
基本使用
<template>
<KbSearch
v-model:mode="mode"
v-model:simple-keyword="keyword"
:modes="modes"
:simple="simpleConfig"
:advanced="advancedConfig"
@simple-search="handleSimpleSearch"
@advanced-search="handleAdvancedSearch"
@simple-select="handleSelect"
@advanced-select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import axios from 'axios'
import { KbSearch } from 'kb-search'
import type {
SearchMode,
SearchParams,
SearchResultItem,
PaginationInfo,
AdvancedSearchField
} from 'kb-search'
const mode = ref<SearchMode>('simple')
const modes = ref<SearchMode[]>(['simple', 'advanced'])
// Simple 模式数据
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
// Advanced 模式数据(示例)
const fields = ref<AdvancedSearchField[]>([])
// 配置对象
const simpleConfig = computed(() => ({
keyword: keyword.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const advancedConfig = computed(() => ({
fields: fields.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const handleSimpleSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑
}
</script>Props(分组)
基础:
mode: SearchMode(v-model)、modes: SearchMode[]
simple 分组(传入 :simple 对象):
{ keyword, loading, error, results, pagination, recommend, conditions, debounce, placeholder }
advanced 分组(传入 :advanced 对象):
{ fields, loading, error, results, pagination, formLayout }
smart 分组(传入 :smart 对象):
{ sessions, currentSessionId, messages, loading, error, showSidebar, askFields, askValues }
Events(命名空间)
update:mode、update:simpleKeywordsimple-search、simple-load-more、simple-selectadvanced-search、advanced-load-more、advanced-selectsmart-send、smart-new-session、smart-switch-session、smart-clear-session、smart-stopupdate:smartAskValues(同步智能提问参数)
Slots(命名空间)
simple-conditions、simple-recommend、simple-recommend-item、simple-resultadvanced-result
类型定义
基础类型
// 搜索模式
type SearchMode = 'simple' | 'advanced' | 'smart'
// 搜索结果项
interface SearchResultItem {
id: string | number
title: string
desc?: string | null
tags?: string[] | null
highlightKws?: string[] | null // 高亮关键词,已转义
author?: string | null
publishTime?: string | Date | null
subtitle?: string | null
[key: string]: any
}
// 分页信息
interface PaginationInfo {
current?: number
pageSize?: number
total?: number
hasMore?: boolean
}
// 推荐块
interface RecommendChunk {
chunkId: string | number
title: string
col: number // 栅格列数,直接对应 24 栅格系统 (1-24)
items: SearchResultItem[]
}
// 搜索条件
interface SearchCondition {
key: string
label: string
type: 'select' | 'input' | 'date' | 'checkbox' | 'radio'
options?: Array<{ label: string; value: any }>
}
// 聊天消息
interface ChatMessage {
id: string | number
role: 'user' | 'assistant'
content: string
}
// 会话信息
interface ChatSession {
id: string | number
title: string
createdAt: number | Date
}完整类型定义请参考源代码:
src/types/index.tssrc/components/SimpleSearch/types.tssrc/components/SmartSearch/types.ts
Hooks
useSearch
提供可选的搜索状态管理 Hook:
import { useSearch } from 'kb-search/hooks/useSearch'
const { keyword, results, loading, error, pagination, onSearch, loadMore, reset } = useSearch({
searchFn: async (keyword, extra) => {
const res = await axios.get('/api/search', { params: { q: keyword, ...extra } })
return { data: res.data.items, total: res.data.total }
},
initialKeyword: '',
pageSize: 10
})返回值:
keyword- 搜索关键词(ref)results- 搜索结果(ref)loading- 加载状态(ref)error- 错误信息(ref)pagination- 分页信息(ref)onSearch- 执行搜索函数loadMore- 加载更多函数reset- 重置函数
最佳实践
按需引入
// ✅ 推荐:只引入需要的组件
import { SimpleSearch } from 'kb-search'
// ❌ 不推荐:引入整个库
import * as KbSearch from 'kb-search'错误处理
<script setup lang="ts">
const handleSearch = async (params) => {
loading.value = true
error.value = null
try {
const res = await axios.get('/api/search', { params })
results.value = res.data.items || []
} catch (err: any) {
error.value = err.message || '搜索失败,请稍后重试'
results.value = []
} finally {
loading.value = false
}
}
</script>流式响应处理(智能搜索)
import { ref } from 'vue'
import type { ChatMessage } from 'kb-search'
const messages = ref<ChatMessage[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const handleSend = async (message: string) => {
loading.value = true
error.value = null
// 添加用户消息
const userMsg: ChatMessage = {
id: Date.now(),
role: 'user',
content: message
}
messages.value.push(userMsg)
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
try {
// 流式响应
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error('无法获取响应流')
}
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
aiMsg.content += chunk
}
} catch (err: any) {
error.value = err.message || '发送失败'
// 移除失败的 AI 消息占位
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
} finally {
loading.value = false
}
}主题定制
/* 全局覆盖 */
.simple-search,
.advanced-search,
.smart-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
}构建与开发
# 开发模式(启动示例项目)
npm run dev
# 构建库
npm run build:lib
# 预览构建结果
npm run preview
# 类型检查
npm run typecheck技术栈
- Vue 3 (Composition API)
- TypeScript
- Arco Design Vue
- Vite
- dayjs
变更日志
v2.0.0
- ✨ 重大变更:所有组件改为纯 UI 组件,不内置请求逻辑
- ✨ 新增
SimpleSearch、AdvancedSearch、SmartSearch独立组件 - ✨ 重构
KbSearch为组合组件 - ✨ 新增
useSearchHook - 🎨 支持 CSS Variables 主题定制
- 📦 支持按需引入,减少打包体积
v1.0.0
- 初始发布
许可证
MIT
