npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kb-search

v2.0.1

Published

A knowledge base search component library based on Vue 3 and Arco Design

Downloads

18

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:modeupdate:simpleKeyword
  • simple-searchsimple-load-moresimple-select
  • advanced-searchadvanced-load-moreadvanced-select
  • smart-sendsmart-new-sessionsmart-switch-sessionsmart-clear-sessionsmart-stop
  • update:smartAskValues(同步智能提问参数)

Slots(命名空间)

  • simple-conditionssimple-recommendsimple-recommend-itemsimple-result
  • advanced-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.ts
  • src/components/SimpleSearch/types.ts
  • src/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 组件,不内置请求逻辑
  • ✨ 新增 SimpleSearchAdvancedSearchSmartSearch 独立组件
  • ✨ 重构 KbSearch 为组合组件
  • ✨ 新增 useSearch Hook
  • 🎨 支持 CSS Variables 主题定制
  • 📦 支持按需引入,减少打包体积

v1.0.0

  • 初始发布

许可证

MIT