提交 248cb0df authored 作者: 龙菲's avatar 龙菲

Initial commit

上级
node_modules
# 开发文件
src/
examples/
docs/
node_modules/
# 配置文件
vite.config.demo.ts
tsconfig.json
.gitignore
.npmignore
# 构建相关
*.map
tsconfig.tsbuildinfo
# 开发工具
.vscode/
.idea/
*.log
.DS_Store
# 测试
coverage/
*.test.ts
*.spec.ts
# 其他
package-lock.json
# NPM 发布指南
本文档说明如何将 `kb-search` 组件库发布到 npm。
## 发布前准备
### 1. 检查 package.json 配置
确保以下字段已正确填写:
- `name`: 包名(已设置为 `kb-search`
- `version`: 版本号(当前为 `1.0.0`
- `author`: 作者信息(请填写你的信息)
- `repository.url`: Git 仓库地址(如果有,请填写)
- `license`: 许可证(已设置为 MIT)
### 2. 确保已构建
发布前需要先构建组件库:
```bash
npm run build:lib
```
这会生成 `dist` 目录,包含所有需要发布的文件。
### 3. 检查构建输出
确保 `dist` 目录包含以下文件:
- `kb-search.js` (ES Module)
- `kb-search.umd.cjs` (UMD)
- `index.d.ts` (TypeScript 类型定义)
- `style.css` (样式文件)
- 其他类型定义文件
## 发布步骤
### 第一步:登录 npm
如果你还没有 npm 账号,请先注册:<https://www.npmjs.com/signup>
然后登录:
```bash
npm login
```
输入你的用户名、密码和邮箱。
### 第二步:检查包名是否可用
在发布前,检查包名是否已被占用:
```bash
npm view kb-search
```
如果返回 404,说明包名可用。如果已被占用,需要修改 `package.json` 中的 `name` 字段。
### 第三步:更新版本号(可选)
如果需要发布新版本,更新版本号:
```bash
# 补丁版本 (1.0.0 -> 1.0.1)
npm version patch
# 次版本 (1.0.0 -> 1.1.0)
npm version minor
# 主版本 (1.0.0 -> 2.0.0)
npm version major
```
或者手动编辑 `package.json` 中的 `version` 字段。
### 第四步:构建组件库
```bash
npm run build:lib
```
### 第五步:预览将要发布的内容
检查哪些文件会被发布:
```bash
npm pack --dry-run
```
这会显示将要打包的文件列表,确保只包含必要的文件。
### 第六步:发布到 npm
#### 发布到公共仓库(默认)
```bash
npm publish
```
#### 发布到测试环境(推荐首次发布时使用)
```bash
npm publish --tag beta
```
这样发布后,用户需要使用 `npm install kb-search@beta` 来安装。
### 第七步:验证发布
发布成功后,可以通过以下方式验证:
```bash
# 查看包信息
npm view kb-search
# 查看版本信息
npm view kb-search versions
# 查看最新版本
npm view kb-search version
```
## 发布后更新
如果需要发布更新版本:
1. 修改代码
2. 更新 `package.json` 中的版本号
3. 运行 `npm run build:lib`
4. 运行 `npm publish`
## 常见问题
### 1. 包名已被占用
如果 `kb-search` 已被占用,可以:
- 使用作用域包名:`@your-username/kb-search`
- 修改为其他名称,如 `kb-search-vue``vue-kb-search`
### 2. 发布权限错误
确保:
- 已正确登录 npm
- 包名未被其他人占用
- 如果是作用域包,需要设置访问权限:`npm publish --access public`
### 3. 构建文件路径错误
如果发布后用户无法正确导入,检查 `package.json` 中的:
- `main`: 指向 UMD 构建文件
- `module`: 指向 ES Module 构建文件
- `types`: 指向类型定义文件
- `exports`: 确保路径正确
### 4. 样式文件未包含
确保 `dist/style.css` 被包含在 `files` 字段中,或者用户需要手动导入样式:
```js
import 'kb-search/dist/style.css'
```
## 发布检查清单
发布前请确认:
- [ ] `package.json` 中的版本号已更新
- [ ] `package.json` 中的 `author``repository` 等信息已填写
- [ ] 已运行 `npm run build:lib` 构建成功
- [ ] `dist` 目录包含所有必要的文件
- [ ] 已运行 `npm pack --dry-run` 检查发布内容
- [ ] 已登录 npm (`npm whoami` 验证)
- [ ] 包名可用(`npm view kb-search` 返回 404)
- [ ] README.md 文档完整
- [ ] 类型定义文件已生成
## 撤销发布(紧急情况)
如果发布后发现严重问题,可以在 72 小时内撤销:
```bash
# 撤销指定版本
npm unpublish kb-search@1.0.0
# 撤销整个包(24小时内)
npm unpublish kb-search --force
```
**注意**:撤销发布会影响包的声誉,请谨慎使用。
## 使用 npm 脚本自动化
`package.json` 中已添加 `prepublishOnly` 脚本,在 `npm publish` 时会自动运行构建,确保发布的是最新构建。
## 后续维护
发布后,建议:
1. 在 GitHub 等平台创建 release tag
2. 更新 CHANGELOG.md(如果有)
3. 通知用户更新
4. 监控 npm 下载量和用户反馈
# KbSearch Component Library
一个基于 Vue 3 + TypeScript + Arco Design 的知识库搜索组件库,支持普通、高级、智能三种搜索模式,开箱即用且高度可定制。
## 特性
- 🔍 **三种搜索模式**:普通搜索、高级搜索、智能搜索(AI 对话式)
- 📱 **响应式设计**:完美适配桌面端和移动端
- 🎨 **主题定制**:支持自定义主题色,通过 CSS Variables 暴露
- 🧩 **纯 UI 组件**:所有组件都是受控组件,不内置请求逻辑,灵活可控
- 📦 **按需引入**:支持独立组件单独引入,减少打包体积
- 🔌 **事件驱动**:所有操作通过 emit 事件通知父组件
- 💬 **智能对话**:支持流式响应、会话历史管理
- 🎯 **推荐功能**:支持热门关键词、推荐内容展示
## 安装
```bash
npm install kb-search
```
### 依赖要求
- Vue 3.3.0+
- @arco-design/web-vue 2.50.0+
- dayjs (用于日期格式化)
> **注意**:组件库不内置 axios 或 fetch,所有数据请求由业务方自行处理。
---
## 快速开始
本组件库提供两种使用方式:
### 方式一:使用独立组件(推荐)
按需引入,打包体积更小:
```vue
<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>
```
### 方式二:使用统一组件(组合组件)
支持多种模式切换:
```vue
<template>
<KbSearch
v-model:mode="mode"
v-model:simpleKeyword="keyword"
:modes="['simple', 'advanced', 'smart']"
:simple="{ keyword, loading, error, results, pagination }"
:advanced="{ fields, loading, error, results, pagination }"
:smart="{ sessions, currentSessionId, messages, loading: smartLoading, error: smartError, askFields, askValues }"
@update:smartAskValues="val => (askValues = val)"
@simple-search="handleSearch"
@advanced-search="handleSearch"
/>
</template>
<script setup lang="ts">
import { KbSearch } from 'kb-search'
import { ref } from 'vue'
const mode = ref('simple')
const keyword = ref('')
const loading = ref(false)
const results = ref([])
const handleSearch = async (params) => {
loading.value = true
// 处理搜索...
loading.value = false
}
</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 事件,不真干活
#### 快速上手
```vue
<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 }
}
})
const handleSearch = ({ keyword, extra }) => {
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 }` |
#### 使用示例
```vue
<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 { SimpleSearch } from 'kb-search'
import type { SearchResultItem, RecommendChunk } from 'kb-search'
const keyword = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
const handleSearch = async ({ keyword: kw, extra }: { keyword: string; extra?: any }) => {
loading.value = true
error.value = null
try {
const res = await axios.get('/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 < res.data.total
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
</script>
```
#### 主题定制
组件使用 CSS Variables 暴露主题变量,业务方可以轻松覆盖:
```css
.simple-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
--search-highlight-bg: #e8f3ff;
--search-highlight-color: #165DFF;
}
```
---
### AdvancedSearch(高级搜索)
#### 定位说明
- ✅ 纯 UI 组件,不内置请求逻辑
-**表单字段完全由外部配置**,支持动态字段配置
-**搜索结果格式与普通搜索一致**,使用 `SearchResultItem` 类型,可复用
- ✅ 支持多种字段类型:文本、下拉、多选、日期、日期范围、数字、开关等
- ✅ 多字段筛选表单
- ✅ 支持标题、内容、标签、日期范围等筛选
#### 基本使用
```vue
<template>
<AdvancedSearch
:fields="fields"
:loading="loading"
:error="error"
@search="handleAdvancedSearch"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { AdvancedSearch } from 'kb-search'
import type { AdvancedSearchField, SearchParams } from 'kb-search'
import type { 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(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('/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 < res.data.total
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
</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 对话式搜索界面
- ✅ 支持会话历史管理(由业务方管理)
- ✅ 支持流式响应显示
#### 基本使用
```vue
<template>
<SmartSearch
:sessions="sessions"
:current-session-id="currentSessionId"
:messages="messages"
:loading="loading"
:error="error"
@send="handleSend"
@new-session="handleNewSession"
@switch-session="handleSwitchSession"
@clear-session="handleClearSession"
@stop="handleStop"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { SmartSearch } from 'kb-search'
import type { ChatSession, ChatMessage } from 'kb-search'
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const handleSend = async (message: string) => {
loading.value = true
try {
// 添加用户消息
messages.value.push({
id: Date.now(),
role: 'user',
content: message
})
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
// 调用 API(支持流式响应)
const res = await axios.post('/api/ai/chat', { message })
aiMsg.content = res.data.content
} catch (err: any) {
error.value = err.message || '发送失败'
} finally {
loading.value = false
}
}
</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 传入。
### 基本使用
```vue
<template>
<KbSearch
v-model:mode="mode"
v-model:keyword="keyword"
:loading="loading"
:error="error"
:results="results"
:pagination="pagination"
:modes="['simple', 'advanced']"
@search="handleSearch"
@select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { KbSearch } from 'kb-search'
import type { SearchMode, SearchParams } from 'kb-search'
const mode = ref<SearchMode>('simple')
const keyword = ref('')
const loading = ref(false)
const results = ref([])
const pagination = ref({ current: 1, pageSize: 10, total: 0, hasMore: false })
const handleSearch = async (params: SearchParams) => {
loading.value = true
try {
const res = await axios.get('/api/search', { params })
results.value = res.data.items || []
pagination.value = {
current: 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: res.data.items.length < res.data.total
}
} finally {
loading.value = false
}
}
</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:simpleKeyword`
- `simple-search``simple-load-more``simple-select`
- `advanced-search``advanced-load-more``advanced-select`
- `smart-send``smart-new-session``smart-switch-session``smart-clear-session``smart-stop`
- `update:smartAskValues`(同步智能提问参数)
#### Slots(命名空间)
- `simple-conditions``simple-recommend``simple-recommend-item``simple-result`
- `advanced-result`
---
## 类型定义
### 基础类型
```ts
// 搜索模式
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:
```ts
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` - 重置函数
---
## 最佳实践
### 按需引入
```ts
// ✅ 推荐:只引入需要的组件
import { SimpleSearch } from 'kb-search'
// ❌ 不推荐:引入整个库
import * as KbSearch from 'kb-search'
```
### 错误处理
```vue
<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>
```
### 流式响应处理(智能搜索)
```ts
const handleSend = async (message: string) => {
// 添加用户消息
messages.value.push({ id: Date.now(), role: 'user', content: message })
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
// 流式响应
const response = await fetch('/api/ai/chat', {
method: 'POST',
body: JSON.stringify({ message })
})
const reader = response.body.getReader()
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
}
}
```
#### 主题定制
```css
/* 全局覆盖 */
.simple-search,
.advanced-search,
.smart-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
}
```
---
## 构建与开发
```bash
# 开发模式(启动示例项目)
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` 为组合组件
- ✨ 新增 `useSearch` Hook
- 🎨 支持 CSS Variables 主题定制
- 📦 支持按需引入,减少打包体积
### v1.0.0
- 初始发布
---
## 许可证
MIT
import { SearchParams } from '../../types';
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<{
config: any;
params: SearchParams;
}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
search: (params: SearchParams) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
config: any;
params: SearchParams;
}>>> & {
onSearch?: ((params: SearchParams) => any) | undefined;
}, {}, {}>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
//# sourceMappingURL=index.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../src/components/AdvancedSearch/index.vue"],"names":[],"mappings":"AAuBA;AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;;YAuQrC,GAAG;YACH,YAAY;;;;YADZ,GAAG;YACH,YAAY;;;;AAPtB,wBAUG;AACH,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC"}
\ No newline at end of file
import { SearchMode, SearchParams } from '../../types';
interface Props {
config: any;
mode: SearchMode;
}
declare function __VLS_template(): {
"advanced-header"?(_: {}): any;
};
declare const __VLS_component: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<Props>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
search: (params: SearchParams) => void;
"mode-change": (mode: SearchMode) => void;
"smart-search": (message: string) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<Props>>> & {
onSearch?: ((params: SearchParams) => any) | undefined;
"onMode-change"?: ((mode: SearchMode) => any) | undefined;
"onSmart-search"?: ((message: string) => any) | undefined;
}, {}, {}>;
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
type __VLS_WithTemplateSlots<T, S> = T & {
new (): {
$slots: S;
};
};
//# sourceMappingURL=KbSearchHeader.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"KbSearchHeader.vue.d.ts","sourceRoot":"","sources":["../../../src/components/KbSearch/KbSearchHeader.vue"],"names":[],"mappings":"AAuCA;AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI3D,UAAU,KAAK;IACb,MAAM,EAAE,GAAG,CAAA;IACX,IAAI,EAAE,UAAU,CAAA;CACjB;AA6CD,iBAAS,cAAc;+BA+IiB,GAAG;EAG1C;AAgBD,QAAA,MAAM,eAAe;;;;;;;;UAOnB,CAAC;wBACkB,uBAAuB,CAAC,OAAO,eAAe,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAAvG,wBAAwG;AACxG,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC;AAC9M,KAAK,uBAAuB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAAE,QAAO;QAClD,MAAM,EAAE,CAAC,CAAC;KACT,CAAA;CAAE,CAAC"}
\ No newline at end of file
import { SearchConfig, SearchResult, SearchParams } from '../../types';
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<{
config: SearchConfig;
}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
search: (params: SearchParams) => void;
"result-click": (item: SearchResult) => void;
"smart-response": (content: string) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
config: SearchConfig;
}>>> & {
onSearch?: ((params: SearchParams) => any) | undefined;
"onResult-click"?: ((item: SearchResult) => any) | undefined;
"onSmart-response"?: ((content: string) => any) | undefined;
}, {}, {}>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
//# sourceMappingURL=index.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../src/components/KbSearch/index.vue"],"names":[],"mappings":"AA6BA;AAGA,OAAO,KAAK,EAAE,YAAY,EAAc,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;;YA0Y7E,YAAY;;;;;;YAAZ,YAAY;;;;;;AANtB,wBASG;AACH,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC"}
\ No newline at end of file
import { SearchConfig } from '../../types';
interface Props {
item: any;
config: SearchConfig;
}
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<Props>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<Props>>>, {}, {}>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
//# sourceMappingURL=DefaultResultItem.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"DefaultResultItem.vue.d.ts","sourceRoot":"","sources":["../../../src/components/SearchResults/DefaultResultItem.vue"],"names":[],"mappings":"AAiCA;AAGA,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,aAAa,CAAA;AAK5D,UAAU,KAAK;IACb,IAAI,EAAE,GAAG,CAAA;IACT,MAAM,EAAE,YAAY,CAAA;CACrB;;AA8ND,wBAMG;AACH,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC"}
\ No newline at end of file
import { SearchResult, SearchConfig } from '../../types';
declare function __VLS_template(): {
"result-item"?(_: {
item: SearchResult;
}): any;
};
declare const __VLS_component: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<{
config: SearchConfig;
results: SearchResult[];
total: number;
loading: boolean;
}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"result-click": (item: SearchResult) => void;
"pagination-change": (page: number) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
config: SearchConfig;
results: SearchResult[];
total: number;
loading: boolean;
}>>> & {
"onResult-click"?: ((item: SearchResult) => any) | undefined;
"onPagination-change"?: ((page: number) => any) | undefined;
}, {}, {}>;
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
type __VLS_WithTemplateSlots<T, S> = T & {
new (): {
$slots: S;
};
};
//# sourceMappingURL=index.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../src/components/SearchResults/index.vue"],"names":[],"mappings":"AAiDA;AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AA2C7D,iBAAS,cAAc;;;QAuRa,GAAG;EAGtC;AAoBD,QAAA,MAAM,eAAe;YAMX,YAAY;aACX,YAAY,EAAE;WAChB,MAAM;aACJ,OAAO;;;;;YAHR,YAAY;aACX,YAAY,EAAE;WAChB,MAAM;aACJ,OAAO;;;;UAGhB,CAAC;wBACkB,uBAAuB,CAAC,OAAO,eAAe,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAAvG,wBAAwG;AACxG,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC;AAC9M,KAAK,uBAAuB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAAE,QAAO;QAClD,MAAM,EAAE,CAAC,CAAC;KACT,CAAA;CAAE,CAAC"}
\ No newline at end of file
import { SearchParams } from '../../types';
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<{
config: any;
params: SearchParams;
}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
search: (params: SearchParams) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
config: any;
params: SearchParams;
}>>> & {
onSearch?: ((params: SearchParams) => any) | undefined;
}, {}, {}>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
//# sourceMappingURL=index.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../src/components/SimpleSearch/index.vue"],"names":[],"mappings":"AAYA;AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;;YAkGrC,GAAG;YACH,YAAY;;;;YADZ,GAAG;YACH,YAAY;;;;AAPtB,wBAUG;AACH,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC"}
\ No newline at end of file
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<{
config: any;
}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
search: (message: string) => void;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
config: any;
}>>> & {
onSearch?: ((message: string) => any) | undefined;
}, {}, {}>;
export default _default;
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
} : {
type: import('vue').PropType<T[K]>;
required: true;
};
};
//# sourceMappingURL=index.vue.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../src/components/SmartSearch/index.vue"],"names":[],"mappings":"AAsBA;;YAuMU,GAAG;;;;YAAH,GAAG;;;;AANb,wBASG;AACH,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AACjE,KAAK,6BAA6B,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE;CAAE,CAAC"}
\ No newline at end of file
import { SearchConfig } from '../types';
export declare const defaultConfig: Partial<SearchConfig>;
export declare const minimalConfigExample: SearchConfig;
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,UAAU,CAAA;AAEzD,eAAO,MAAM,aAAa,EAAE,OAAO,CAAC,YAAY,CAgB/C,CAAA;AAED,eAAO,MAAM,oBAAoB,EAU5B,YAAY,CAAA"}
\ No newline at end of file
import { App } from 'vue';
export { default as KbSearch } from './components/KbSearch/index.vue';
export * from './types';
export { defaultConfig, minimalConfigExample } from './config';
export { GlobalSearch };
declare const _default: {
install(app: App): void;
};
export default _default;
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG/B,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AACrE,cAAc,SAAS,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAE9D,OAAO,EAAE,YAAY,EAAE,CAAC;;iBAGT,GAAG;;AADlB,wBAME"}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
import { VNode } from 'vue';
export type SearchMode = 'simple' | 'advanced' | 'smart';
export interface FieldConfig {
key: string;
label: string;
type?: 'text' | 'date' | 'tag' | 'number';
searchable?: boolean;
formatter?: (value: any, item: SearchResult) => string | VNode;
render?: (value: any, item: SearchResult) => VNode;
}
export interface ApiConfig {
search: string | ((params: any) => Promise<{
data: SearchResult[];
total: number;
}>);
smart?: string | ((message: string) => Promise<{
content: string;
}>);
recommend?: string | ((params: any) => Promise<{
data: SearchResult[];
}>);
}
export interface DisplayConfig {
fields: FieldConfig[];
layout?: 'list' | 'grid' | 'card';
perPage?: number;
showDetail?: boolean;
}
export interface SearchConfig {
api: ApiConfig;
display: DisplayConfig;
modes?: SearchMode[];
theme?: {
primaryColor?: string;
};
slots?: {
header?: string;
resultItem?: string;
empty?: string;
loading?: string;
};
}
export interface SearchResult {
id?: string;
title?: string;
content?: string;
description?: string;
tags?: string[];
date?: string | Date;
[key: string]: any;
}
export interface SearchParams {
keyword?: string;
filters?: Record<string, any>;
page?: number;
pageSize?: number;
mode?: SearchMode;
}
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,CAAA;AAEhC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,KAAK,CAAC;IAC/D,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,KAAK,KAAK,CAAC;CACpD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACrF,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACrE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,SAAS,CAAC;IACf,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE;QACN,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB"}
\ No newline at end of file
# KbSearch Component Library
一个基于 Vue 3 + TypeScript + Arco Design 的知识库搜索组件库,支持普通、高级、智能三种搜索模式,开箱即用且高度可定制。
## 特性
- 🔍 **三种搜索模式**:普通搜索、高级搜索、智能搜索(AI 对话式)
- 📱 **响应式设计**:完美适配桌面端和移动端
- 🎨 **主题定制**:支持自定义主题色,通过 CSS Variables 暴露
- 🧩 **纯 UI 组件**:所有组件都是受控组件,不内置请求逻辑,灵活可控
- 📦 **按需引入**:支持独立组件单独引入,减少打包体积
- 🔌 **事件驱动**:所有操作通过 emit 事件通知父组件
- 💬 **智能对话**:支持流式响应、会话历史管理
- 🎯 **推荐功能**:支持热门关键词、推荐内容展示
## 安装
```bash
npm install kb-search
```
### 依赖要求
- Vue 3.3.0+
- @arco-design/web-vue 2.50.0+
- dayjs (用于日期格式化)
> **注意**:组件库不内置 axios 或 fetch,所有数据请求由业务方自行处理。
---
## 快速开始
本组件库提供两种使用方式:
### 方式一:使用独立组件(推荐)
按需引入,打包体积更小:
```vue
<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>
```
### 方式二:使用统一组件(组合组件)
支持多种模式切换:
```vue
<template>
<KbSearch
v-model:mode="mode"
v-model:simpleKeyword="keyword"
:modes="['simple', 'advanced', 'smart']"
:simple="{ keyword, loading, error, results, pagination }"
:advanced="{ fields, loading, error, results, pagination }"
:smart="{ sessions, currentSessionId, messages, loading: smartLoading, error: smartError, askFields, askValues }"
@update:smartAskValues="val => (askValues = val)"
@simple-search="handleSearch"
@advanced-search="handleSearch"
/>
</template>
<script setup lang="ts">
import { KbSearch } from 'kb-search'
import { ref } from 'vue'
const mode = ref('simple')
const keyword = ref('')
const loading = ref(false)
const results = ref([])
const handleSearch = async (params) => {
loading.value = true
// 处理搜索...
loading.value = false
}
</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 事件,不真干活
#### 快速上手
```vue
<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 }
}
})
const handleSearch = ({ keyword, extra }) => {
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 }` |
#### 使用示例
```vue
<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 { SimpleSearch } from 'kb-search'
import type { SearchResultItem, RecommendChunk } from 'kb-search'
const keyword = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
const handleSearch = async ({ keyword: kw, extra }: { keyword: string; extra?: any }) => {
loading.value = true
error.value = null
try {
const res = await axios.get('/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 < res.data.total
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
</script>
```
#### 主题定制
组件使用 CSS Variables 暴露主题变量,业务方可以轻松覆盖:
```css
.simple-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
--search-highlight-bg: #e8f3ff;
--search-highlight-color: #165DFF;
}
```
---
### AdvancedSearch(高级搜索)
#### 定位说明
- ✅ 纯 UI 组件,不内置请求逻辑
-**表单字段完全由外部配置**,支持动态字段配置
-**搜索结果格式与普通搜索一致**,使用 `SearchResultItem` 类型,可复用
- ✅ 支持多种字段类型:文本、下拉、多选、日期、日期范围、数字、开关等
- ✅ 多字段筛选表单
- ✅ 支持标题、内容、标签、日期范围等筛选
#### 基本使用
```vue
<template>
<AdvancedSearch
:fields="fields"
:loading="loading"
:error="error"
@search="handleAdvancedSearch"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { AdvancedSearch } from 'kb-search'
import type { AdvancedSearchField, SearchParams } from 'kb-search'
import type { 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(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('/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 < res.data.total
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
</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 对话式搜索界面
- ✅ 支持会话历史管理(由业务方管理)
- ✅ 支持流式响应显示
#### 基本使用
```vue
<template>
<SmartSearch
:sessions="sessions"
:current-session-id="currentSessionId"
:messages="messages"
:loading="loading"
:error="error"
@send="handleSend"
@new-session="handleNewSession"
@switch-session="handleSwitchSession"
@clear-session="handleClearSession"
@stop="handleStop"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { SmartSearch } from 'kb-search'
import type { ChatSession, ChatMessage } from 'kb-search'
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const handleSend = async (message: string) => {
loading.value = true
try {
// 添加用户消息
messages.value.push({
id: Date.now(),
role: 'user',
content: message
})
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
// 调用 API(支持流式响应)
const res = await axios.post('/api/ai/chat', { message })
aiMsg.content = res.data.content
} catch (err: any) {
error.value = err.message || '发送失败'
} finally {
loading.value = false
}
}
</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 传入。
### 基本使用
```vue
<template>
<KbSearch
v-model:mode="mode"
v-model:keyword="keyword"
:loading="loading"
:error="error"
:results="results"
:pagination="pagination"
:modes="['simple', 'advanced']"
@search="handleSearch"
@select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { KbSearch } from 'kb-search'
import type { SearchMode, SearchParams } from 'kb-search'
const mode = ref<SearchMode>('simple')
const keyword = ref('')
const loading = ref(false)
const results = ref([])
const pagination = ref({ current: 1, pageSize: 10, total: 0, hasMore: false })
const handleSearch = async (params: SearchParams) => {
loading.value = true
try {
const res = await axios.get('/api/search', { params })
results.value = res.data.items || []
pagination.value = {
current: 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: res.data.items.length < res.data.total
}
} finally {
loading.value = false
}
}
</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:simpleKeyword`
- `simple-search``simple-load-more``simple-select`
- `advanced-search``advanced-load-more``advanced-select`
- `smart-send``smart-new-session``smart-switch-session``smart-clear-session``smart-stop`
- `update:smartAskValues`(同步智能提问参数)
#### Slots(命名空间)
- `simple-conditions``simple-recommend``simple-recommend-item``simple-result`
- `advanced-result`
---
## 类型定义
### 基础类型
```ts
// 搜索模式
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:
```ts
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` - 重置函数
---
## 最佳实践
### 按需引入
```ts
// ✅ 推荐:只引入需要的组件
import { SimpleSearch } from 'kb-search'
// ❌ 不推荐:引入整个库
import * as KbSearch from 'kb-search'
```
### 错误处理
```vue
<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>
```
### 流式响应处理(智能搜索)
```ts
const handleSend = async (message: string) => {
// 添加用户消息
messages.value.push({ id: Date.now(), role: 'user', content: message })
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
id: Date.now() + 1,
role: 'assistant',
content: ''
}
messages.value.push(aiMsg)
// 流式响应
const response = await fetch('/api/ai/chat', {
method: 'POST',
body: JSON.stringify({ message })
})
const reader = response.body.getReader()
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
}
}
```
#### 主题定制
```css
/* 全局覆盖 */
.simple-search,
.advanced-search,
.smart-search {
--search-primary-color: #165DFF;
--search-border-radius: 8px;
--search-gap: 16px;
}
```
---
## 构建与开发
```bash
# 开发模式(启动示例项目)
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` 为组合组件
- ✨ 新增 `useSearch` Hook
- 🎨 支持 CSS Variables 主题定制
- 📦 支持按需引入,减少打包体积
### v1.0.0
- 初始发布
---
## 许可证
MIT
<template>
<div class="app-preview">
<header class="preview-header">
<h1>KnowledgeSearch 组件实时预览</h1>
<p>
演示 SimpleSearch
组件的所有功能:搜索、推荐、分页、条件筛选、插槽自定义等
</p>
</header>
<main class="search-demo">
<KbSearch
v-model:mode="mode"
v-model:simpleKeyword="keyword"
:modes="['simple', 'advanced', 'smart']"
:simple="{
keyword,
loading: loading,
error: error,
results: results,
pagination: pagination,
recommend: recommend,
conditions: conditions,
debounce: 300,
placeholder: '输入关键词搜索知识库...',
}"
:advanced="{
fields: advancedFields,
formLayout: advancedFormLayout,
loading: loading,
error: error,
results: results,
pagination: pagination,
}"
:smart="{
sessions: [],
currentSessionId: undefined,
messages: [],
loading: false,
error: null,
showSidebar: true,
askFields: smartAskFields,
askValues: smartAskValues,
}"
@update:smartAskValues="(val) => (smartAskValues = val)"
@simple-search="handleSearch"
@advanced-search="handleSearch"
@simple-load-more="handleLoadMore"
@advanced-load-more="handleLoadMore"
@simple-select="handleSelect"
@advanced-select="handleSelect"
>
<!-- 自定义条件区域 -->
<template #simple-conditions="{ conditions }">
<div class="conditions-wrapper">
<a-select
v-model="selectedCategory"
placeholder="选择分类"
style="width: 200px; margin-right: 12px"
allow-clear
@change="handleCategoryChange"
>
<a-option value="">全部</a-option>
<a-option value="tech">技术</a-option>
<a-option value="business">商业</a-option>
<a-option value="design">设计</a-option>
</a-select>
<a-select
v-model="selectedType"
placeholder="选择类型"
style="width: 200px"
allow-clear
@change="handleTypeChange"
>
<a-option value="">全部</a-option>
<a-option value="article">文章</a-option>
<a-option value="video">视频</a-option>
<a-option value="course">课程</a-option>
</a-select>
</div>
</template>
<!-- 自定义结果卡片 -->
<template #simple-result="{ item }">
<div class="custom-result-card" @click="handleSelect(item)">
<div class="result-header">
<h3 class="result-title" v-html="highlightTitle(item)"></h3>
<a-tag v-if="item.category" color="blue">{{
getCategoryLabel(item.category)
}}</a-tag>
</div>
<div v-if="item.subtitle" class="result-subtitle">
{{ item.subtitle }}
</div>
<div
v-if="item.desc"
class="result-desc"
v-html="highlightDesc(item)"
></div>
<div class="result-meta">
<span v-if="item.author" class="meta-item">
<icon-user /> {{ item.author }}
</span>
<span v-if="item.publishTime" class="meta-item">
<icon-clock-circle /> {{ formatTime(item.publishTime) }}
</span>
<span v-if="item.tags && item.tags.length" class="meta-tags">
<a-tag
v-for="tag in item.tags"
:key="tag"
size="small"
color="arcoblue"
>
{{ tag }}
</a-tag>
</span>
</div>
</div>
</template>
</KbSearch>
</main>
<footer v-if="loading" class="status-footer">
<a-spin />
<span>搜索中...</span>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { Message } from "@arco-design/web-vue";
import { IconUser, IconClockCircle } from "@arco-design/web-vue/es/icon";
import KbSearch from "../src/components/KbSearch/index.vue";
import type { SearchMode, SearchParams } from "../src/types/index";
import type {
SearchResultItem,
RecommendChunk,
PaginationInfo,
SearchEventParams,
} from "../src/components/SimpleSearch/types";
import type { AdvancedSearchField } from "../src/components/AdvancedSearch/types";
import dayjs from "dayjs";
// 状态管理
const mode = ref<SearchMode>("simple");
const keyword = ref("");
const loading = ref(false);
const error = ref<string | null>(null);
const results = ref<SearchResultItem[]>([]);
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false,
});
// 条件筛选
const selectedCategory = ref("");
const selectedType = ref("");
// 推荐数据
const recommend = ref<RecommendChunk[]>([]);
// 高级搜索:字段配置与表单布局
const advancedFields = ref<AdvancedSearchField[]>([
{ key: "title", label: "标题", type: "text", placeholder: "输入标题关键词" },
{
key: "content",
label: "内容",
type: "text",
placeholder: "输入内容关键词",
},
{
key: "tags",
label: "标签",
type: "multi-select",
options: [
{ label: "Vue", value: "Vue" },
{ label: "React", value: "React" },
{ label: "TypeScript", value: "TypeScript" },
{ label: "工程化", value: "工程化" },
{ label: "设计", value: "设计" },
],
},
{
key: "dateRange",
label: "日期范围",
type: "date-range",
placeholder: ["开始日期", "结束日期"],
},
{ key: "fuzzy", label: "模糊检索", type: "switch", defaultValue: true },
{
key: "categoryValue",
label: "类别",
type: "select",
options: [
{ label: "全部", value: "all" },
{ label: "Vue", value: "Vue" },
{ label: "Arco", value: "Arco" },
{ label: "Vite", value: "Vite" },
],
},
]);
const advancedFormLayout = ref({
labelCol: 3,
wrapperCol: 18,
labelAlign: "right" as const,
});
// 智能搜索:提问参数面板配置(示例)
const smartAskFields = ref([
{ key: "topic", label: "主题", type: "text", placeholder: "请输入主题" },
{
key: "tone",
label: "语气",
type: "select",
options: [
{ label: "正式", value: "formal" },
{ label: "友好", value: "friendly" },
{ label: "学术", value: "academic" },
],
},
{
key: "keywords",
label: "关键词",
type: "multi-select",
options: [
{ label: "性能", value: "性能" },
{ label: "优化", value: "优化" },
{ label: "架构", value: "架构" },
],
},
{
key: "urgency",
label: "紧急程度",
type: "slider",
props: { min: 0, max: 10 },
},
{
key: "attachments",
label: "附件",
type: "file",
upload: {
action: "/api/upload",
method: "POST",
fieldName: "file",
headers: { Authorization: "Bearer token" },
data: { biz: "kb-smart" },
multiple: true,
accept: ".pdf,.docx,image/*",
limit: 3,
},
},
]);
const smartAskValues = ref<Record<string, any>>({});
// 搜索条件配置
const conditions = ref([
{
key: "category",
label: "分类",
type: "select",
options: [
{ label: "全部", value: "" },
{ label: "技术", value: "tech" },
{ label: "商业", value: "business" },
{ label: "设计", value: "design" },
],
},
{
key: "type",
label: "类型",
type: "select",
options: [
{ label: "全部", value: "" },
{ label: "文章", value: "article" },
{ label: "视频", value: "video" },
{ label: "课程", value: "course" },
],
},
]);
// 测试数据:搜索结果
const mockResults: SearchResultItem[] = [
{
id: 1,
title: "Vue 3 Composition API 完整指南",
subtitle: "深入理解 Vue 3 的核心特性",
desc: "本指南将详细介绍 Vue 3 的 Composition API,包括 setup 函数、响应式 API、生命周期钩子等核心概念。通过学习本指南,您将能够熟练使用 Composition API 构建复杂的 Vue 应用。",
tags: ["Vue", "前端", "框架"],
author: "张三",
publishTime: "2024-01-15",
category: "tech",
type: "article",
highlightKws: ["Vue", "API"],
},
{
id: 2,
title: "TypeScript 在大型项目中的应用实践",
subtitle: "提升代码质量和开发效率",
desc: "本文将分享如何在大型前端项目中有效使用 TypeScript,包括类型设计、接口定义、泛型使用等最佳实践。通过实际案例,帮助开发者更好地利用 TypeScript 的类型系统。",
tags: ["TypeScript", "编程", "最佳实践"],
author: "李四",
publishTime: "2024-01-20",
category: "tech",
type: "article",
highlightKws: ["TypeScript"],
},
{
id: 3,
title: "现代前端工程化实践",
subtitle: "从零到一构建高效开发流程",
desc: "探讨现代前端工程化的各个方面,包括构建工具、模块化、代码规范、自动化测试等。帮助团队建立一套完整的前端工程化体系,提升开发效率和代码质量。",
tags: ["工程化", "前端", "开发"],
author: "王五",
publishTime: "2024-02-01",
category: "tech",
type: "course",
highlightKws: ["前端"],
},
{
id: 4,
title: "产品设计思维与方法论",
subtitle: "打造用户喜爱的产品",
desc: "介绍产品设计的核心思维和方法论,包括用户研究、需求分析、原型设计、用户体验优化等。通过实际案例,帮助产品设计师和开发者更好地理解用户需求。",
tags: ["设计", "产品", "用户体验"],
author: "赵六",
publishTime: "2024-02-10",
category: "design",
type: "article",
highlightKws: ["设计"],
},
{
id: 5,
title: "创业公司如何做好市场推广",
subtitle: "低成本高效果的营销策略",
desc: "分享创业公司在资源有限的情况下,如何通过精准的市场定位、内容营销、社交媒体推广等方式,实现低成本高效果的市场推广。适合初创企业的创始人阅读。",
tags: ["创业", "营销", "商业"],
author: "孙七",
publishTime: "2024-02-15",
category: "business",
type: "article",
highlightKws: ["市场"],
},
{
id: 6,
title: "React Hooks 深度解析",
subtitle: "掌握 React 函数式组件开发",
desc: "深入解析 React Hooks 的工作原理和使用技巧,包括 useState、useEffect、useContext、自定义 Hooks 等。帮助开发者更好地使用 Hooks 构建 React 应用。",
tags: ["React", "前端", "Hooks"],
author: "周八",
publishTime: "2024-02-20",
category: "tech",
type: "video",
highlightKws: ["React"],
},
{
id: 7,
title: "UI/UX 设计趋势 2024",
subtitle: "了解最新的设计趋势",
desc: "总结 2024 年 UI/UX 设计的主要趋势,包括新拟态设计、深色模式、微交互、无障碍设计等。帮助设计师了解行业动态,提升设计水平。",
tags: ["设计", "UI", "UX"],
author: "吴九",
publishTime: "2024-02-25",
category: "design",
type: "article",
highlightKws: ["设计"],
},
{
id: 8,
title: "企业数字化转型指南",
subtitle: "传统企业如何拥抱数字化",
desc: "探讨传统企业如何进行数字化转型,包括技术选型、组织变革、流程优化等。通过实际案例,帮助企业领导者制定数字化转型战略。",
tags: ["商业", "数字化", "转型"],
author: "郑十",
publishTime: "2024-03-01",
category: "business",
type: "course",
highlightKws: ["数字化"],
},
{
id: 9,
title: "Node.js 后端开发最佳实践",
subtitle: "构建高性能的 Node.js 应用",
desc: "分享 Node.js 后端开发的最佳实践,包括异步编程、错误处理、性能优化、安全防护等。帮助后端开发者构建稳定、高效、安全的 Node.js 应用。",
tags: ["Node.js", "后端", "服务器"],
author: "钱十一",
publishTime: "2024-03-05",
category: "tech",
type: "article",
highlightKws: ["Node.js"],
},
{
id: 10,
title: "移动端适配方案全解析",
subtitle: "打造完美的移动端体验",
desc: "详细介绍移动端适配的各种方案,包括响应式设计、rem/em 方案、viewport 设置、flexible 方案等。帮助前端开发者解决移动端适配问题。",
tags: ["移动端", "前端", "适配"],
author: "陈十二",
publishTime: "2024-03-10",
category: "tech",
type: "article",
highlightKws: ["移动端"],
},
];
// 测试数据:推荐内容
const mockRecommend: RecommendChunk[] = [
{
chunkId: 1,
title: "🔥 热门推荐",
col: 24, // 全宽 (24 栅格系统)
items: [
{
id: 101,
title: "Vue 3 从入门到精通",
desc: "系统学习 Vue 3 的所有特性,成为 Vue 专家",
tags: ["Vue", "教程"],
},
{
id: 102,
title: "TypeScript 实战指南",
desc: "通过实际项目掌握 TypeScript 开发技巧",
tags: ["TypeScript", "实战"],
},
],
},
{
chunkId: 2,
title: "📚 精选文章",
col: 12, // 一半宽度 (24 栅格系统)
items: [
{
id: 201,
title: "前端性能优化实战",
desc: "10 个实用的性能优化技巧",
tags: ["性能", "优化"],
},
{
id: 202,
title: "CSS 新特性详解",
desc: "探索 CSS 的最新功能和用法",
tags: ["CSS", "前端"],
},
],
},
{
chunkId: 3,
title: "🎥 视频教程",
col: 8, // 三分之一宽度 (24 栅格系统)
items: [
{
id: 301,
title: "React 18 新特性",
desc: "了解 React 18 带来的新变化",
tags: ["React", "视频"],
},
],
},
{
chunkId: 4,
title: "💡 最新动态",
col: 8, // 三分之一宽度 (24 栅格系统)
items: [
{
id: 401,
title: "Web3 开发入门",
desc: "探索 Web3 和区块链开发",
tags: ["Web3", "区块链"],
},
],
},
];
// 初始化推荐数据
recommend.value = mockRecommend;
// 模拟搜索 API
const mockSearch = (
searchKeyword: string,
extra?: Record<string, any>
): Promise<{ data: SearchResultItem[]; total: number }> => {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟搜索延迟
let data = [...mockResults];
// 简单模式:关键词过滤
if (searchKeyword) {
const kw = searchKeyword.toLowerCase();
data = data.filter(
(it) =>
(it.title || "").toLowerCase().includes(kw) ||
(it.desc || "").toLowerCase().includes(kw) ||
(it.tags || []).some((t) => (t || "").toLowerCase().includes(kw))
);
data = data.map((it) => ({ ...it, highlightKws: [searchKeyword] }));
}
// 高级模式:字段过滤(与 demo 高级逻辑一致)
const {
title = "",
content = "",
tags = [],
dateRange = [],
fuzzy = true,
categoryValue,
category,
type,
} = extra || {};
const matchText = (text: string, pattern: string) => {
if (!pattern) return true;
if (!text) return false;
return fuzzy
? text.toLowerCase().includes(pattern.toLowerCase())
: text === pattern;
};
// 标题/内容匹配
if (title || content) {
data = data.filter(
(it) =>
matchText(it.title || "", title) &&
matchText(it.desc || "", content)
);
}
// 标签(全部包含)
if (Array.isArray(tags) && tags.length) {
data = data.filter(
(it) =>
Array.isArray(it.tags) &&
tags.every((t: string) => (it.tags as string[]).includes(t))
);
}
// 日期范围(YYYY-MM-DD 或 Date)
if (Array.isArray(dateRange) && dateRange.length === 2) {
const [start, end] = dateRange;
const startTime = new Date(start).getTime();
const endTime = new Date(end).getTime();
data = data.filter((it) => {
const ts = new Date(it.publishTime as any).getTime();
return (
(isNaN(startTime) || ts >= startTime) &&
(isNaN(endTime) || ts <= endTime)
);
});
}
// 类别与类型(支持 simple 条件与 advanced 类别)
const cat =
category ??
(categoryValue && categoryValue !== "all" ? categoryValue : "");
if (cat) {
data = data.filter(
(it) => (it.tags || []).includes(cat) || it.category === cat
);
}
if (type) {
data = data.filter((it) => it.type === type);
}
resolve({ data, total: data.length });
}, 800); // 模拟网络延迟
});
};
// 搜索处理
const handleSearch = async (params: SearchParams) => {
loading.value = true;
error.value = null;
try {
const extra: Record<string, any> = {};
if (selectedCategory.value) {
extra.category = selectedCategory.value;
}
if (selectedType.value) {
extra.type = selectedType.value;
}
// 从 SearchParams 中提取 keyword(KbSearch 会传递 keyword 字段)
const searchKeyword = params.keyword || keyword.value || "";
const response = await mockSearch(searchKeyword, {
...(params.filters || {}),
...extra,
});
results.value = response.data;
pagination.value = {
current: 1,
pageSize: 10,
total: response.total,
hasMore: response.data.length < response.total,
};
// 搜索时隐藏推荐
if (searchKeyword) {
recommend.value = [];
} else {
recommend.value = mockRecommend;
}
} catch (err: any) {
error.value = err.message || "搜索失败,请稍后重试";
results.value = [];
} finally {
loading.value = false;
}
};
// 加载更多
const handleLoadMore = async () => {
if (loading.value || !pagination.value.hasMore) return;
loading.value = true;
try {
// 模拟加载更多数据
await new Promise((resolve) => setTimeout(resolve, 500));
// 这里简化处理,实际应该加载下一页数据
pagination.value.hasMore = false;
} catch (err: any) {
error.value = err.message || "加载失败";
} finally {
loading.value = false;
}
};
// 选择结果
const handleSelect = (item: SearchResultItem) => {
console.log("选中结果:", item);
Message.success(`查看详情: ${item.title}`);
const detailUrl = `data:text/html,<html><body><h1>${item.title}</h1><p>${
item.desc || ""
}</p><p>作者: ${item.author || "未知"}</p><p>发布时间: ${
item.publishTime || "未知"
}</p><p>标签: ${item.tags?.join(", ") || ""}</p></body></html>`;
window.open(detailUrl, "_blank");
};
// 分类变化
const handleCategoryChange = (value: string) => {
selectedCategory.value = value;
if (keyword.value) {
handleSearch({ keyword: keyword.value, mode: "simple" });
}
};
// 类型变化
const handleTypeChange = (value: string) => {
selectedType.value = value;
if (keyword.value) {
handleSearch({ keyword: keyword.value, mode: "simple" });
}
};
// 格式化时间
const formatTime = (time: string | Date | null | undefined): string => {
if (!time) return "";
try {
return dayjs(time).format("YYYY-MM-DD HH:mm");
} catch {
return String(time);
}
};
// 获取分类标签
const getCategoryLabel = (category: string): string => {
const map: Record<string, string> = {
tech: "技术",
business: "商业",
design: "设计",
};
return map[category] || category;
};
// 高亮标题
const highlightTitle = (item: SearchResultItem): string => {
let title = item.title || "";
if (item.highlightKws && item.highlightKws.length > 0) {
item.highlightKws.forEach((kw) => {
const regex = new RegExp(`(${kw})`, "gi");
title = title.replace(
regex,
`<mark style="background: #e8f3ff; color: #165DFF; padding: 2px 4px; border-radius: 2px;">$1</mark>`
);
});
}
return title;
};
// 高亮描述
const highlightDesc = (item: SearchResultItem): string => {
let desc = item.desc || "";
if (item.highlightKws && item.highlightKws.length > 0) {
item.highlightKws.forEach((kw) => {
const regex = new RegExp(`(${kw})`, "gi");
desc = desc.replace(
regex,
`<mark style="background: #e8f3ff; color: #165DFF; padding: 2px 4px; border-radius: 2px;">$1</mark>`
);
});
}
return desc;
};
// 监听关键词变化,清空时显示推荐
watch(keyword, (newVal) => {
if (!newVal) {
recommend.value = mockRecommend;
results.value = [];
}
});
</script>
<style scoped>
.app-preview {
min-height: 100vh;
background: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.preview-header {
text-align: center;
padding: 20px;
background: white;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-header h1 {
color: var(--color-primary, #165dff);
margin: 0 0 8px;
font-size: 24px;
}
.preview-header p {
color: var(--color-text-2, #637381);
margin: 0;
font-size: 14px;
}
.search-demo {
/* max-width: 1200px; */
margin: 0 auto;
/* padding: 0 20px; */
flex: 1;
}
.conditions-wrapper {
display: flex;
margin-top: 12px;
gap: 12px;
}
.custom-result-card {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #e5e6eb;
margin-bottom: 16px;
cursor: pointer;
transition: all 0.2s;
}
.custom-result-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #165dff;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.result-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1d2129;
flex: 1;
line-height: 1.5;
}
.result-subtitle {
font-size: 14px;
color: #86909c;
margin-bottom: 8px;
}
.result-desc {
font-size: 14px;
color: #4e5969;
line-height: 1.6;
margin-bottom: 12px;
}
.result-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
font-size: 13px;
color: #86909c;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.meta-tags {
display: flex;
gap: 6px;
}
.status-footer {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: white;
color: var(--color-text-2, #637381);
gap: 8px;
}
@media (max-width: 768px) {
.preview-header {
padding: 16px;
}
.search-demo {
padding: 0 10px;
}
.conditions-wrapper {
flex-direction: column;
}
.conditions-wrapper .a-select {
width: 100% !important;
margin-right: 0 !important;
}
}
</style>
<template>
<div id="app">
<KbSearch
:config="config"
@search="onSearch"
@result-click="onResultClick"
@smart-response="onSmartResponse"
>
<template #result-item="{ item }">
<div class="custom-result">
<h3>{{ item.title }}</h3>
<p class="description">{{ item.description }}</p>
<div class="actions">
<a-button size="small" @click="onResultClick(item)">查看</a-button>
<a-button size="small" type="secondary" @click="shareItem(item)"
>分享</a-button
>
</div>
</div>
</template>
<template #empty>
<div class="custom-empty">
<a-icon name="database" />
<p>没有找到相关内容,尝试调整搜索条件</p>
</div>
</template>
</KbSearch>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import KbSearch from "../src/index.vue";
import type { SearchConfig, SearchResult } from "../src/types";
const config: SearchConfig = {
api: {
search: async (params) => {
// Mock advanced search
return {
data: [
{
id: 1,
title: "高级搜索示例",
description: "使用多字段过滤的搜索结果。",
tags: ["高级", "搜索"],
date: new Date(),
},
],
total: 1,
};
},
smart: async (message) => {
return { content: `AI响应: 您的问题是 "${message}",这是模拟响应。` };
},
recommend: "/api/recommend",
},
display: {
fields: [
{ key: "title", label: "标题" },
{ key: "description", label: "描述" },
{ key: "tags", label: "标签", type: "tag" },
{
key: "date",
label: "更新时间",
type: "date",
formatter: (val) => new Date(val).toLocaleString(),
},
],
layout: "card",
perPage: 5,
},
modes: ["simple", "advanced", "smart"],
theme: {
primaryColor: "#722ED1",
},
};
const onSearch = (params: any) => {
console.log("Search event:", params);
};
const onResultClick = (item: SearchResult) => {
console.log("Result clicked:", item);
};
const onSmartResponse = (content: string) => {
console.log("AI response:", content);
};
const shareItem = (item: SearchResult) => {
console.log("Share:", item);
};
</script>
<style>
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.custom-result {
padding: 16px;
}
.custom-result h3 {
margin: 0 0 8px 0;
color: var(--color-primary);
}
.description {
color: var(--color-text-2);
margin-bottom: 12px;
line-height: 1.6;
}
.actions {
display: flex;
gap: 8px;
}
.custom-empty {
text-align: center;
padding: 40px;
color: var(--color-text-3);
}
.custom-empty p {
margin-top: 8px;
}
</style>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KbSearch Component Preview</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f0f2f5;
}
#app {
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
import { createApp } from 'vue'
import App from './App.vue' // 相对 examples/
import '@arco-design/web-vue/dist/arco.css'
import ArcoVue from '@arco-design/web-vue'
import { KbSearch } from '../src/index.ts' // 从 examples/ 到 src/
console.log('Demo main.ts loaded, imported { KbSearch }: ', KbSearch)
const app = createApp(App)
app.use(ArcoVue)
app.component('KbSearch', KbSearch)
console.log('KbSearch registered, mounting App...')
app.mount('#app')
console.log('App mounted')
<template>
<div id="app">
<KbSearch :config="config" @result-click="onResultClick" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import KbSearch from '../src/index.vue'
import { minimalConfigExample } from '../src/config'
import type { SearchResult } from '../src/types'
const config = ref({
...minimalConfigExample,
api: {
search: async (params: any) => {
// Mock API for demo
console.log('Search params:', params)
return {
data: [
{
id: 1,
title: 'Vue 3 Composition API 指南',
description: '深入了解 Vue 3 的 Composition API,使用方式和最佳实践。',
tags: ['Vue', 'Composition API'],
date: '2024-01-01'
},
{
id: 2,
title: 'TypeScript 在 Vue 中的应用',
description: '如何在 Vue 项目中集成 TypeScript,提升代码质量和开发效率。',
tags: ['TypeScript', 'Vue'],
date: '2024-01-15'
}
],
total: 100
}
}
}
})
const onResultClick = (item: SearchResult) => {
console.log('Clicked result:', item)
alert(`查看详情: ${item.title}`)
}
</script>
<style>
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
</style>
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
interface ComponentCustomProps {
compilerOptions?: {
isCustomElement?: (tag: string) => boolean
}
}
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '*.ts' {
const src: any
export default src
}
interface ImportMetaEnv {
readonly VITE_DEMO?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KbSearch Component Preview</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f0f2f5;
}
#app {
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./examples/main.ts"></script>
</body>
</html>
{
"name": "kb-search",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kb-search",
"version": "1.0.0",
"dependencies": {
"axios": "^1.6.0",
"dayjs": "^1.11.10"
},
"devDependencies": {
"@types/node": "^20.16.1",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/tsconfig": "^0.5.1",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vite-plugin-dts": "^3.8.1",
"vue-tsc": "^2.0.7"
},
"peerDependencies": {
"@arco-design/web-vue": "^2.50.0",
"vue": "^3.3.0"
}
},
"node_modules/@arco-design/color": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/@arco-design/color/-/color-0.4.0.tgz",
"integrity": "sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==",
"peer": true,
"dependencies": {
"color": "^3.1.3"
}
},
"node_modules/@arco-design/web-vue": {
"version": "2.56.0",
"resolved": "https://registry.npmmirror.com/@arco-design/web-vue/-/web-vue-2.56.0.tgz",
"integrity": "sha512-LsrTE1vL54a/DVQCZ4c2F5LDA1r2mcWF2AHrM+fKEi5hzE63/awZVIOQ6P8yGaYRAP9eqUNj60uNI1Jz0UvGlA==",
"peer": true,
"dependencies": {
"@arco-design/color": "^0.4.0",
"b-tween": "^0.3.3",
"b-validate": "^1.4.4",
"compute-scroll-into-view": "^1.0.17",
"dayjs": "^1.10.3",
"number-precision": "^1.5.0",
"resize-observer-polyfill": "^1.5.1",
"scroll-into-view-if-needed": "^2.2.28"
},
"peerDependencies": {
"vue": "^3.1.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"node_modules/@microsoft/api-extractor": {
"version": "7.43.0",
"resolved": "https://registry.npmmirror.com/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz",
"integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==",
"dev": true,
"dependencies": {
"@microsoft/api-extractor-model": "7.28.13",
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "~0.16.1",
"@rushstack/node-core-library": "4.0.2",
"@rushstack/rig-package": "0.5.2",
"@rushstack/terminal": "0.10.0",
"@rushstack/ts-command-line": "4.19.1",
"lodash": "~4.17.15",
"minimatch": "~3.0.3",
"resolve": "~1.22.1",
"semver": "~7.5.4",
"source-map": "~0.6.1",
"typescript": "5.4.2"
},
"bin": {
"api-extractor": "bin/api-extractor"
}
},
"node_modules/@microsoft/api-extractor-model": {
"version": "7.28.13",
"resolved": "https://registry.npmmirror.com/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz",
"integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==",
"dev": true,
"dependencies": {
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "~0.16.1",
"@rushstack/node-core-library": "4.0.2"
}
},
"node_modules/@microsoft/api-extractor/node_modules/typescript": {
"version": "5.4.2",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/@microsoft/tsdoc": {
"version": "0.14.2",
"resolved": "https://registry.npmmirror.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==",
"dev": true
},
"node_modules/@microsoft/tsdoc-config": {
"version": "0.16.2",
"resolved": "https://registry.npmmirror.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz",
"integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==",
"dev": true,
"dependencies": {
"@microsoft/tsdoc": "0.14.2",
"ajv": "~6.12.6",
"jju": "~1.4.0",
"resolve": "~1.19.0"
}
},
"node_modules/@microsoft/tsdoc-config/node_modules/resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"dev": true,
"dependencies": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rushstack/node-core-library": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz",
"integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==",
"dev": true,
"dependencies": {
"fs-extra": "~7.0.1",
"import-lazy": "~4.0.0",
"jju": "~1.4.0",
"resolve": "~1.22.1",
"semver": "~7.5.4",
"z-schema": "~5.0.2"
},
"peerDependencies": {
"@types/node": "*"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@rushstack/rig-package": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/@rushstack/rig-package/-/rig-package-0.5.2.tgz",
"integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==",
"dev": true,
"dependencies": {
"resolve": "~1.22.1",
"strip-json-comments": "~3.1.1"
}
},
"node_modules/@rushstack/terminal": {
"version": "0.10.0",
"resolved": "https://registry.npmmirror.com/@rushstack/terminal/-/terminal-0.10.0.tgz",
"integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==",
"dev": true,
"dependencies": {
"@rushstack/node-core-library": "4.0.2",
"supports-color": "~8.1.1"
},
"peerDependencies": {
"@types/node": "*"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@rushstack/ts-command-line": {
"version": "4.19.1",
"resolved": "https://registry.npmmirror.com/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz",
"integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==",
"dev": true,
"dependencies": {
"@rushstack/terminal": "0.10.0",
"@types/argparse": "1.0.38",
"argparse": "~1.0.9",
"string-argv": "~0.3.1"
}
},
"node_modules/@types/argparse": {
"version": "1.0.38",
"resolved": "https://registry.npmmirror.com/@types/argparse/-/argparse-1.0.38.tgz",
"integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.19.24",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.24.tgz",
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
"dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dev": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@volar/language-core": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz",
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
"dev": true,
"dependencies": {
"@volar/source-map": "1.11.1"
}
},
"node_modules/@volar/source-map": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz",
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
"dev": true,
"dependencies": {
"muggle-string": "^0.3.1"
}
},
"node_modules/@volar/typescript": {
"version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz",
"integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
"dev": true,
"dependencies": {
"@volar/language-core": "2.4.15",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/@volar/typescript/node_modules/@volar/language-core": {
"version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
"dev": true,
"dependencies": {
"@volar/source-map": "2.4.15"
}
},
"node_modules/@volar/typescript/node_modules/@volar/source-map": {
"version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
"dev": true
},
"node_modules/@vue/compiler-core": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.0.tgz",
"integrity": "sha512-cw4S15PkNGTKkP9OFFl4wnQoJJk+HqaYBafgrpDnSukiQGpcYJeRpzmqnCVCIkl6V6Eqsv58E0OAdl6b592vuA==",
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.0",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.0.tgz",
"integrity": "sha512-E957uOhpoE48YjZGWeAoLmNYd3UeU4oIP8kJi8Rcsb9l2tV8Z48Jn07Zgq1aW0v3vuhlmydEKkKKbhLpADHXEA==",
"dependencies": {
"@vue/compiler-core": "3.4.0",
"@vue/shared": "3.4.0"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.0.tgz",
"integrity": "sha512-PWE0mE2yW7bJS7PmaCrVDEG6KPaDJo0pb4AKnCxJ5lRRDO4IwL/fswBGhCpov+v/c+N/e+hQHpXNwvqU9BtUXg==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.0",
"@vue/compiler-dom": "3.4.0",
"@vue/compiler-ssr": "3.4.0",
"@vue/shared": "3.4.0",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.0.tgz",
"integrity": "sha512-+oXKy105g9DIYQKDi3Gwung0xqQX5gJHr0GR+Vf7yK/WkNDM6q61ummcKmKAB85EIst8y3vj2PA9z9YU5Oc4DQ==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.4.0",
"@vue/shared": "3.4.0"
}
},
"node_modules/@vue/compiler-vue2": {
"version": "2.7.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/@vue/language-core": {
"version": "1.8.27",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
"dev": true,
"dependencies": {
"@volar/language-core": "~1.11.1",
"@volar/source-map": "~1.11.1",
"@vue/compiler-dom": "^3.3.0",
"@vue/shared": "^3.3.0",
"computeds": "^0.0.1",
"minimatch": "^9.0.3",
"muggle-string": "^0.3.1",
"path-browserify": "^1.0.1",
"vue-template-compiler": "^2.7.14"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@vue/language-core/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@vue/language-core/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vue/reactivity": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.0.tgz",
"integrity": "sha512-X6BvQjNcgKKHWPQzlRJjZvIu72Kkn8xJSv6VNptqWh8dToMknD0Hch1l4N7llKgVt6Diq4lMeUnErbZFvuGlAA==",
"peer": true,
"dependencies": {
"@vue/shared": "3.4.0"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.0.tgz",
"integrity": "sha512-NYrj/JgMMqnSWcIud8lLzDQrBLu+EVEeQ56QE9DYJeKG2eFrnQy8o/h57R9nCprafHs0uImKL3xsdXjHseYVxw==",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.4.0",
"@vue/shared": "3.4.0"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.0.tgz",
"integrity": "sha512-1ZoHEsA5l77qbx2F+SWo/hQdBksPuOmww1t/jznidDG+xMB/iidafEFvo2ZTtZii0JfTIrlDhjshfYUvQC17wQ==",
"peer": true,
"dependencies": {
"@vue/runtime-core": "3.4.0",
"@vue/shared": "3.4.0",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.0.tgz",
"integrity": "sha512-GuOVCyLDlWPu8nKo5AUxb8B+iB/Ik4I1WwqAlBqf5+y48z6D6rvKshp7KR3cJea+pte1tdTsb0+Ja82KizMZOw==",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.0",
"@vue/shared": "3.4.0"
},
"peerDependencies": {
"vue": "3.4.0"
}
},
"node_modules/@vue/shared": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.0.tgz",
"integrity": "sha512-Nhh3ed3G1R6HDAWiG6YYFt0Zmq/To6u5vjzwa9TIquGheCXPY6nEdIAO8ZdlwXsWqC2yNLj700FOvShpYt5CEA=="
},
"node_modules/@vue/tsconfig": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
"dev": true
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
"dev": true
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b-tween": {
"version": "0.3.3",
"resolved": "https://registry.npmmirror.com/b-tween/-/b-tween-0.3.3.tgz",
"integrity": "sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==",
"peer": true
},
"node_modules/b-validate": {
"version": "1.5.3",
"resolved": "https://registry.npmmirror.com/b-validate/-/b-validate-1.5.3.tgz",
"integrity": "sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==",
"peer": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"peer": true,
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"peer": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"peer": true
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"peer": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"peer": true
},
"node_modules/computeds": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/import-lazy": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/import-lazy/-/import-lazy-4.0.0.tgz",
"integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"peer": true
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jju": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/jju/-/jju-1.4.0.tgz",
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"dev": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"dev": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.0.8",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.0.8.tgz",
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/muggle-string": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz",
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/number-precision": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz",
"integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==",
"peer": true
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"peer": true
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.5",
"@rollup/rollup-android-arm64": "4.52.5",
"@rollup/rollup-darwin-arm64": "4.52.5",
"@rollup/rollup-darwin-x64": "4.52.5",
"@rollup/rollup-freebsd-arm64": "4.52.5",
"@rollup/rollup-freebsd-x64": "4.52.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
"@rollup/rollup-linux-arm64-musl": "4.52.5",
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
"@rollup/rollup-linux-x64-gnu": "4.52.5",
"@rollup/rollup-linux-x64-musl": "4.52.5",
"@rollup/rollup-openharmony-arm64": "4.52.5",
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
"@rollup/rollup-win32-x64-gnu": "4.52.5",
"@rollup/rollup-win32-x64-msvc": "4.52.5",
"fsevents": "~2.3.2"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"peer": true,
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"peer": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/validator": {
"version": "13.15.20",
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.20.tgz",
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
"dev": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
},
"node_modules/vite-plugin-dts": {
"version": "3.8.1",
"resolved": "https://registry.npmmirror.com/vite-plugin-dts/-/vite-plugin-dts-3.8.1.tgz",
"integrity": "sha512-zEYyQxH7lKto1VTKZHF3ZZeOPkkJgnMrePY4VxDHfDSvDjmYMMfWjZxYmNwW8QxbaItWJQhhXY+geAbyNphI7g==",
"dev": true,
"dependencies": {
"@microsoft/api-extractor": "7.43.0",
"@rollup/pluginutils": "^5.1.0",
"@vue/language-core": "^1.8.27",
"debug": "^4.3.4",
"kolorist": "^1.8.0",
"magic-string": "^0.30.8",
"vue-tsc": "^1.8.27"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"typescript": "*",
"vite": "*"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vite-plugin-dts/node_modules/@volar/typescript": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz",
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
"dev": true,
"dependencies": {
"@volar/language-core": "1.11.1",
"path-browserify": "^1.0.1"
}
},
"node_modules/vite-plugin-dts/node_modules/vue-tsc": {
"version": "1.8.27",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz",
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
"dev": true,
"dependencies": {
"@volar/typescript": "~1.11.1",
"@vue/language-core": "1.8.27",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": "*"
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true
},
"node_modules/vue": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.0.tgz",
"integrity": "sha512-iTE9Ve/7DO/H39+gXHrNkRdnh1jDwPe/fap4brbPKkp1APMkS03OiZ+UY0dwpqtRX0iPWQTkh8Fu3hKgLtaxfA==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.4.0",
"@vue/compiler-sfc": "3.4.0",
"@vue/runtime-dom": "3.4.0",
"@vue/server-renderer": "3.4.0",
"@vue/shared": "3.4.0"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-template-compiler": {
"version": "2.7.16",
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/vue-tsc": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
"integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
"dev": true,
"dependencies": {
"@volar/typescript": "2.4.15",
"@vue/language-core": "2.2.12"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/vue-tsc/node_modules/@volar/language-core": {
"version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
"dev": true,
"dependencies": {
"@volar/source-map": "2.4.15"
}
},
"node_modules/vue-tsc/node_modules/@volar/source-map": {
"version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
"dev": true
},
"node_modules/vue-tsc/node_modules/@vue/compiler-core": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/shared": "3.5.22",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/vue-tsc/node_modules/@vue/compiler-dom": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.5.22",
"@vue/shared": "3.5.22"
}
},
"node_modules/vue-tsc/node_modules/@vue/language-core": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
"integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
"dev": true,
"dependencies": {
"@volar/language-core": "2.4.15",
"@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.0",
"alien-signals": "^1.0.3",
"minimatch": "^9.0.3",
"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-tsc/node_modules/@vue/shared": {
"version": "3.5.22",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
"dev": true
},
"node_modules/vue-tsc/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/vue-tsc/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/vue-tsc/node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmmirror.com/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"dev": true,
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
}
}
}
{
"name": "kb-search",
"version": "1.0.0",
"description": "A knowledge base search component library based on Vue 3 and Arco Design",
"type": "module",
"main": "./dist/kb-search.umd.cjs",
"module": "./dist/kb-search.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/kb-search.js",
"require": "./dist/kb-search.umd.cjs"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"dev": "vite --config vite.config.demo.ts --open",
"build:lib": "vue-tsc --noEmit && vite build",
"preview": "vite preview examples --port 5050",
"typecheck": "vue-tsc --noEmit",
"prepublishOnly": "npm run build:lib"
},
"keywords": [
"vue",
"vue3",
"search",
"component",
"arco-design",
"knowledge-base",
"ui-library"
],
"author": "longfei0128@foxmail.com",
"license": "MIT",
"repository": {
"type": "git",
"url": ""
},
"peerDependencies": {
"vue": "^3.3.0",
"@arco-design/web-vue": "^2.50.0"
},
"devDependencies": {
"@types/node": "^20.16.1",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/tsconfig": "^0.5.1",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vite-plugin-dts": "^3.8.1",
"vue-tsc": "^2.0.7"
},
"dependencies": {
"axios": "^1.6.0",
"dayjs": "^1.11.10"
}
}
\ No newline at end of file
<template>
<div class="advanced-form-wrap">
<a-form
class="adv-form"
layout="horizontal"
:model="formData"
@submit="handleSearch"
:label-align="formLayout.labelAlign"
:label-col-props="{ span: formLayout.labelCol }"
:wrapper-col-props="{ span: formLayout.wrapperCol }"
>
<!-- 动态渲染表单字段 -->
<a-form-item
v-for="field in fields"
:key="field.key"
:label="field.label"
:required="field.required"
>
<!-- 文本输入框 -->
<a-input
v-if="field.type === 'text'"
v-model="formData[field.key]"
:placeholder="getPlaceholder(field) || `请输入${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<!-- 数字输入框 -->
<a-input-number
v-else-if="field.type === 'number'"
v-model="formData[field.key]"
:placeholder="getPlaceholder(field) || `请输入${field.label}`"
:disabled="field.disabled || loading"
style="width: 100%"
v-bind="field.props || {}"
/>
<!-- 单选下拉框 -->
<a-select
v-else-if="field.type === 'select'"
v-model="formData[field.key]"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
allow-clear
style="width: 100%"
v-bind="field.props || {}"
>
<a-option
v-for="option in field.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
<!-- 多选下拉框 -->
<a-select
v-else-if="field.type === 'multi-select'"
v-model="formData[field.key]"
mode="multiple"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
allow-clear
style="width: 100%"
v-bind="field.props || {}"
>
<a-option
v-for="option in field.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
<!-- 日期选择器 -->
<a-date-picker
v-else-if="field.type === 'date'"
v-model="formData[field.key]"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
style="width: 100%"
v-bind="field.props || {}"
/>
<!-- 日期范围选择器 -->
<a-range-picker
v-else-if="field.type === 'date-range'"
v-model="formData[field.key]"
:placeholder="
Array.isArray(field.placeholder)
? field.placeholder
: field.placeholder
? [field.placeholder, field.placeholder]
: [`开始日期`, `结束日期`]
"
:disabled="field.disabled || loading"
style="width: 100%"
v-bind="field.props || {}"
/>
<!-- 开关 -->
<a-switch
v-else-if="field.type === 'switch'"
v-model="formData[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<!-- 复选框 -->
<a-checkbox
v-else-if="field.type === 'checkbox'"
v-model="formData[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>
{{ field.label }}
</a-checkbox>
<!-- 单选按钮组 -->
<a-radio-group
v-else-if="field.type === 'radio'"
v-model="formData[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>
<a-radio
v-for="option in field.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-radio>
</a-radio-group>
</a-form-item>
<!-- 操作按钮 -->
<a-form-item
:label-col-props="{ span: formLayout.labelCol }"
:wrapper-col-props="{ span: formLayout.wrapperCol }"
>
<a-button type="primary" html-type="submit" :loading="loading">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button
style="margin-left: 12px"
@click="resetForm"
:disabled="loading"
>
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-form-item>
</a-form>
<!-- 搜索结果区域(高级搜索组件自行渲染) -->
<div class="results-section">
<div
v-if="loading && (!resultsList || resultsList.length === 0)"
class="loading-state"
>
<a-spin />
</div>
<div
v-else-if="error && (!resultsList || resultsList.length === 0)"
class="error-state"
>
<a-alert type="error" :message="error || ''" />
</div>
<div
v-else-if="!resultsList || resultsList.length === 0"
class="empty-state"
>
<a-empty description="暂无搜索结果" />
</div>
<div v-else class="results-list">
<slot
name="result"
:item="item"
v-for="item in resultsList"
:key="item.id"
>
<!-- 默认结果渲染(兜底) -->
<div class="default-result-card" @click="onSelect(item)">
<div class="title">{{ item.title || item.id }}</div>
<div v-if="item.desc" class="desc">{{ item.desc }}</div>
<div class="meta">
<span v-if="item.author">{{ item.author }}</span>
<span v-if="item.publishTime" style="margin-left: 12px">{{
formatTime(item.publishTime)
}}</span>
</div>
</div>
</slot>
</div>
<div v-if="showLoadMore" class="load-more">
<a-button
:loading="loading"
type="primary"
size="large"
@click="onLoadMore"
>加载更多</a-button
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import type { SearchParams } from "../../types";
import { IconSearch, IconRefresh } from "@arco-design/web-vue/es/icon";
import type {
AdvancedSearchProps,
AdvancedSearchEmits,
AdvancedSearchField,
} from "./types";
/**
* 高级搜索组件
*
* 功能说明:
* 1. 支持通过 fields 配置动态渲染表单字段
* 2. 支持多种字段类型:text、select、multi-select、date、date-range、number、switch、checkbox、radio
* 3. 搜索结果格式与普通搜索一致,使用 SearchResultItem 类型
* 4. 通过 emit('search', SearchParams) 触发搜索,父组件负责处理搜索逻辑和结果渲染
*
* 使用示例:
* ```vue
* <AdvancedSearch
* :fields="fields"
* :loading="loading"
* @search="handleAdvancedSearch"
* />
*
* // fields 配置示例:
* const fields = [
* { key: 'title', label: '标题', type: 'text', placeholder: '请输入标题' },
* { key: 'category', label: '分类', type: 'select', options: [
* { label: '全部', value: '' },
* { label: '技术', value: 'tech' }
* ]},
* { key: 'tags', label: '标签', type: 'multi-select', options: [...] },
* { key: 'dateRange', label: '日期范围', type: 'date-range' },
* { key: 'fuzzy', label: '模糊检索', type: 'switch', defaultValue: true }
* ]
* ```
*/
const props = withDefaults(defineProps<AdvancedSearchProps>(), {
fields: () => [],
loading: false,
error: null,
formLayout: () => ({
labelCol: 3,
wrapperCol: 18,
labelAlign: "right" as const,
}),
results: () => [],
pagination: undefined,
});
const emit = defineEmits<AdvancedSearchEmits>();
/**
* 初始化表单数据
* 根据 fields 配置生成初始值,支持 defaultValue
*/
const initFormData = (): Record<string, any> => {
const data: Record<string, any> = {};
props.fields.forEach((field) => {
// 根据字段类型设置默认值
if (field.defaultValue !== undefined) {
data[field.key] = field.defaultValue;
} else {
switch (field.type) {
case "multi-select":
case "date-range":
data[field.key] = [];
break;
case "switch":
case "checkbox":
data[field.key] = false;
break;
case "number":
data[field.key] = undefined;
break;
default:
data[field.key] = "";
}
}
});
return data;
};
const formData = ref<Record<string, any>>(initFormData());
/**
* 监听 fields 变化,重新初始化表单数据
*/
watch(
() => props.fields,
() => {
formData.value = initFormData();
},
{ deep: true }
);
/**
* 处理搜索
* 将表单数据转换为 SearchParams 格式并触发搜索事件
*/
const handleSearch = () => {
const params: SearchParams = {
filters: { ...formData.value },
page: 1,
};
emit("search", params);
};
/**
* 重置表单
* 将表单数据重置为初始值
*/
const resetForm = () => {
formData.value = initFormData();
};
// 结果相关
const resultsList = computed(() => props.results || []);
const showLoadMore = computed(() => {
if (!props.pagination) return false;
return props.pagination.hasMore !== false && resultsList.value.length > 0;
});
const onLoadMore = () => emit("load-more");
const onSelect = (item: any) => emit("select", item);
const formatTime = (time: any): string => {
if (!time) return "";
try {
const d = new Date(time);
if (isNaN(d.getTime())) return String(time);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const da = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${da}`;
} catch {
return String(time);
}
};
// 占位符兜底(除 date-range 外,仅接受 string)
const getPlaceholder = (field: AdvancedSearchField): string | undefined => {
return typeof field.placeholder === "string" ? field.placeholder : undefined;
};
</script>
<style scoped>
.advanced-form-wrap {
width: 100%;
}
.adv-form {
width: 50%;
margin: 0 auto;
}
@media (max-width: 992px) {
.adv-form {
width: 100%;
}
}
.results-section {
width: 100%;
margin-top: 16px;
}
.loading-state,
.error-state,
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.results-list {
width: 100%;
}
.default-result-card {
background: #fff;
padding: 16px;
border-radius: 8px;
border: 1px solid #e5e6eb;
margin-bottom: 12px;
cursor: pointer;
}
.default-result-card .title {
font-weight: 600;
color: #1d2129;
margin-bottom: 6px;
}
.default-result-card .desc {
font-size: 14px;
color: #4e5969;
margin-bottom: 6px;
}
.default-result-card .meta {
font-size: 12px;
color: #86909c;
}
.load-more {
display: flex;
justify-content: center;
padding: 24px 0;
}
</style>
/**
* 高级搜索组件类型定义
*/
/**
* 高级搜索表单字段类型
*/
export type AdvancedFieldType =
| 'text' // 文本输入框
| 'select' // 单选下拉框
| 'multi-select' // 多选下拉框
| 'date' // 单个日期选择器
| 'date-range' // 日期范围选择器
| 'number' // 数字输入框
| 'switch' // 开关(布尔值)
| 'checkbox' // 复选框(布尔值)
| 'radio'; // 单选按钮组
/**
* 高级搜索表单字段配置
*/
export interface AdvancedSearchField {
/** 字段唯一标识(对应后端字段名) */
key: string;
/** 字段标签(显示名称) */
label: string;
/** 字段类型 */
type: AdvancedFieldType;
/** 占位符(可选,date-range 类型支持数组格式) */
placeholder?: string | [string, string];
/** 选项列表(用于 select、multi-select、radio 类型) */
options?: Array<{ label: string; value: any }>;
/** 默认值 */
defaultValue?: any;
/** 是否必填 */
required?: boolean;
/** 验证规则(可选,用于未来扩展) */
rules?: any[];
/** 是否禁用 */
disabled?: boolean;
/** 额外的组件属性(传递给具体的表单控件) */
props?: Record<string, any>;
}
/**
* 高级搜索组件 Props
*/
export interface AdvancedSearchProps {
/** 表单字段配置列表 */
fields: AdvancedSearchField[];
/** 加载状态 */
loading?: boolean;
/** 错误信息 */
error?: string | null;
/** 搜索结果(与普通搜索一致) */
results?: import('../SimpleSearch/types').SearchResultItem[];
/** 分页信息 */
pagination?: import('../SimpleSearch/types').PaginationInfo;
/** 表单布局配置 */
formLayout?: {
/** 标签宽度(栅格列数,默认 3) */
labelCol?: number;
/** 输入框宽度(栅格列数,默认 18) */
wrapperCol?: number;
/** 标签对齐方式(默认 'right') */
labelAlign?: 'left' | 'right';
};
}
/**
* 高级搜索组件 Emits
*/
export interface AdvancedSearchEmits {
/** 搜索事件,参数为 SearchParams */
(e: 'search', params: import('../../types').SearchParams): void;
/** 加载更多 */
(e: 'load-more'): void;
/** 选择结果 */
(e: 'select', item: import('../SimpleSearch/types').SearchResultItem): void;
}
<template>
<div class="search-header">
<a-tabs
v-model:activeKey="activeMode"
@change="handleModeChange"
size="large"
v-if="showModeSwitcher"
>
<a-tab-pane key="simple" tab="普通搜索">
<a-input-search
v-model:value="keyword"
placeholder="搜索知识库..."
@search="handleSearch"
allow-clear
size="large"
style="width: 100%; margin-top: 12px"
/>
</a-tab-pane>
<a-tab-pane key="advanced" tab="高级搜索">
<slot name="advanced-header" />
</a-tab-pane>
<a-tab-pane key="smart" tab="智能搜索">
<a-input-textarea
v-model:value="aiMessage"
placeholder="与AI对话搜索..."
@keydown.enter="handleSmartSearch"
:rows="3"
style="width: 100%; margin-top: 12px"
/>
</a-tab-pane>
</a-tabs>
<!-- Default simple search if no tabs -->
<a-input-search
v-else
v-model:value="keyword"
placeholder="搜索知识库..."
@search="handleSearch"
allow-clear
size="large"
style="width: 100%"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import type { SearchMode, SearchParams } from "../../types";
interface Props {
config: any;
mode: SearchMode;
}
interface Emits {
(e: "search", params: SearchParams): void;
(e: "mode-change", mode: SearchMode): void;
(e: "smart-search", message: string): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const activeMode = ref(props.mode);
const keyword = ref("");
const aiMessage = ref("");
const showModeSwitcher = computed(
() => props.config.modes && props.config.modes.length > 1
);
watch(
() => props.mode,
(newMode) => {
activeMode.value = newMode;
}
);
const handleModeChange = (key: string | number) => {
const mode = key as SearchMode;
activeMode.value = mode;
emit("mode-change", mode);
keyword.value = "";
aiMessage.value = "";
};
const handleSearch = () => {
if (keyword.value.trim()) {
emit("search", { keyword: keyword.value.trim(), mode: activeMode.value });
}
};
const handleSmartSearch = () => {
if (aiMessage.value.trim()) {
emit("smart-search", aiMessage.value.trim());
aiMessage.value = "";
}
};
</script>
<style scoped>
.search-header {
margin-bottom: 24px;
}
@media (max-width: 768px) {
.search-header :deep(.arco-tabs-nav) {
flex-wrap: wrap;
}
.search-header :deep(.arco-input) {
--input-height: 44px;
}
}
</style>
<template>
<div class="kb-search">
<!-- 搜索区域(白色背景,满屏宽) -->
<section class="search-section">
<div class="container">
<!-- 模式切换:顶部居中 Radio -->
<div v-if="showModeSwitch" class="mode-switch">
<a-radio-group
type="button"
size="large"
:model-value="currentMode"
@change="handleModeChange"
>
<a-radio v-for="mode in availableModes" :key="mode" :value="mode">
{{ modeLabels[mode] }}
</a-radio>
</a-radio-group>
</div>
<!-- 根据模式渲染对应内容 -->
<div v-if="currentMode === 'simple'" class="simple-area">
<SimpleSearch
v-model:keyword="simpleKeyword"
:loading="simpleCfg.loading"
:error="simpleCfg.error"
:results="simpleCfg.results"
:pagination="simpleCfg.pagination"
:recommend="simpleCfg.recommend"
:conditions="simpleCfg.conditions"
:debounce="simpleCfg.debounce"
:placeholder="simpleCfg.placeholder"
@search="onSimpleSearch"
@load-more="onSimpleLoadMore"
@select="onSimpleSelect"
>
<template #conditions="{ conditions }">
<slot name="simple-conditions" :conditions="conditions"></slot>
</template>
<template #recommend="{ recommend }">
<slot name="simple-recommend" :recommend="recommend"></slot>
</template>
<template #recommend-item="{ chunk, items }">
<slot
name="simple-recommend-item"
:chunk="chunk"
:items="items"
></slot>
</template>
<template #result="{ item }">
<slot name="simple-result" :item="item"></slot>
</template>
</SimpleSearch>
</div>
<div v-else-if="currentMode === 'advanced'" class="advanced-area">
<div class="content-wrap">
<AdvancedSearch
:fields="advancedCfg.fields"
:loading="advancedCfg.loading"
:error="advancedCfg.error"
:results="advancedCfg.results"
:pagination="advancedCfg.pagination"
:form-layout="advancedCfg.formLayout"
@search="onAdvancedSearch"
@load-more="onAdvancedLoadMore"
@select="onAdvancedSelect"
>
<template #result="{ item }">
<slot name="advanced-result" :item="item"></slot>
</template>
</AdvancedSearch>
</div>
</div>
<div v-else-if="currentMode === 'smart'" class="smart-area">
<div class="content-wrap">
<SmartSearch
:sessions="smartCfg.sessions"
:current-session-id="smartCfg.currentSessionId"
:messages="smartCfg.messages"
:loading="smartCfg.loading"
:error="smartCfg.error"
:show-sidebar="smartCfg.showSidebar"
:ask-fields="smartCfg.askFields"
:ask-values="smartCfg.askValues"
@send="handleSmartSend"
@new-session="handleSmartNewSession"
@switch-session="handleSmartSwitchSession"
@clear-session="handleSmartClearSession"
@stop="handleSmartStop"
@update:askValues="(val) => emit('update:smartAskValues', val)"
/>
</div>
</div>
</div>
</section>
<!-- 高级搜索结果由 AdvancedSearch 内部渲染;此处不再重复渲染 -->
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import type { SearchMode, SearchResult, SearchParams } from "../../types";
import SimpleSearch from "../SimpleSearch/index.vue";
import AdvancedSearch from "../AdvancedSearch/index.vue";
import SmartSearch from "../SmartSearch/index.vue";
import type {
SearchResultItem,
RecommendChunk,
PaginationInfo,
} from "../SimpleSearch/types";
import type { ChatSession, ChatMessage } from "../SmartSearch/types";
import type { SmartAskField } from "../SmartSearch/types";
import type { AdvancedSearchField } from "../AdvancedSearch/types";
interface Props {
/** 当前模式 */
mode?: SearchMode;
/** 可用模式列表 */
modes?: SearchMode[];
/** simple 模式配置对象 */
simple?: {
keyword?: string;
loading?: boolean;
error?: string | null;
results?: SearchResultItem[];
pagination?: PaginationInfo;
recommend?: RecommendChunk[];
conditions?: any[];
debounce?: number;
placeholder?: string;
};
/** advanced 模式配置对象 */
advanced?: {
fields?: AdvancedSearchField[];
loading?: boolean;
error?: string | null;
results?: SearchResultItem[];
pagination?: PaginationInfo;
formLayout?: {
labelCol?: number;
wrapperCol?: number;
labelAlign?: "left" | "right";
};
};
/** smart 模式配置对象 */
smart?: {
sessions?: ChatSession[];
currentSessionId?: string | number;
messages?: ChatMessage[];
loading?: boolean;
error?: string | null;
showSidebar?: boolean;
askFields?: SmartAskField[];
askValues?: Record<string, any>;
};
}
const props = withDefaults(defineProps<Props>(), {
mode: "simple",
modes: () => ["simple", "advanced", "smart"],
simple: () => ({
keyword: "",
loading: false,
error: null,
results: [],
pagination: undefined,
recommend: [],
conditions: [],
debounce: 300,
placeholder: "输入关键词搜索...",
}),
advanced: () => ({
fields: [],
loading: false,
error: null,
results: [],
pagination: undefined,
formLayout: { labelCol: 3, wrapperCol: 18, labelAlign: "right" as const },
}),
smart: () => ({
sessions: [],
currentSessionId: undefined,
messages: [],
loading: false,
error: null,
showSidebar: true,
}),
});
const emit = defineEmits<{
/** 模式切换 */
"update:mode": [mode: SearchMode];
/** simple 关键词变化 */
"update:simpleKeyword": [keyword: string];
/** simple/advanced 搜索与交互 */
"simple-search": [params: SearchParams];
"advanced-search": [params: SearchParams];
"simple-load-more": [];
"advanced-load-more": [];
"simple-select": [item: SearchResultItem];
"advanced-select": [item: SearchResultItem];
/** 智能搜索发送消息 */
"smart-send": [message: string];
/** 智能搜索新建会话 */
"smart-new-session": [];
/** 智能搜索切换会话 */
"smart-switch-session": [sessionId: string | number];
/** 智能搜索清空会话 */
"smart-clear-session": [sessionId: string | number];
/** 智能搜索停止生成 */
"smart-stop": [];
/** 更新智能搜索提问参数 */
"update:smartAskValues": [values: Record<string, any>];
}>();
const currentMode = ref<SearchMode>(props.mode);
const simpleKeyword = ref(props.simple.keyword || "");
const modeLabels: Record<SearchMode, string> = {
simple: "普通搜索",
advanced: "高级搜索",
smart: "智能搜索",
};
const availableModes = computed(() => {
return props.modes.filter((m) => ["simple", "advanced", "smart"].includes(m));
});
const showModeSwitch = computed(() => {
return availableModes.value.length > 1;
});
// 模式切换
const handleModeChange = (mode: string | number | boolean) => {
const modeValue = mode as string | number;
currentMode.value = modeValue as SearchMode;
emit("update:mode", modeValue as SearchMode);
};
// simple 模式事件
const onSimpleSearch = (params: { keyword: string; extra?: any }) => {
simpleKeyword.value = params.keyword;
emit("update:simpleKeyword", params.keyword);
emit("simple-search", {
keyword: params.keyword,
mode: "simple",
...params.extra,
});
};
// advanced 模式事件
const onAdvancedSearch = (params: SearchParams) => {
emit("advanced-search", {
...params,
mode: "advanced",
});
};
const onSimpleLoadMore = () => emit("simple-load-more");
const onAdvancedLoadMore = () => emit("advanced-load-more");
const onSimpleSelect = (item: SearchResultItem) => emit("simple-select", item);
const onAdvancedSelect = (item: SearchResultItem) =>
emit("advanced-select", item);
// 智能搜索事件
const handleSmartSend = (message: string) => {
emit("smart-send", message);
};
const handleSmartNewSession = () => {
emit("smart-new-session");
};
const handleSmartSwitchSession = (sessionId: string | number) => {
emit("smart-switch-session", sessionId);
};
const handleSmartClearSession = (sessionId: string | number) => {
emit("smart-clear-session", sessionId);
};
const handleSmartStop = () => {
emit("smart-stop");
};
// 便捷 computed:汇总配置(提供默认值)
const simpleCfg = computed(() => ({
loading: props.simple?.loading ?? false,
error: props.simple?.error ?? null,
results: props.simple?.results ?? [],
pagination: props.simple?.pagination,
recommend: props.simple?.recommend ?? [],
conditions: props.simple?.conditions ?? [],
debounce: props.simple?.debounce ?? 300,
placeholder: props.simple?.placeholder ?? "输入关键词搜索...",
}));
const advancedCfg = computed(() => ({
fields: props.advanced?.fields ?? [],
loading: props.advanced?.loading ?? false,
error: props.advanced?.error ?? null,
results: props.advanced?.results ?? [],
pagination: props.advanced?.pagination,
formLayout: props.advanced?.formLayout ?? {
labelCol: 3,
wrapperCol: 18,
labelAlign: "right" as const,
},
}));
const smartCfg = computed(() => ({
sessions: props.smart?.sessions ?? [],
currentSessionId: props.smart?.currentSessionId,
messages: props.smart?.messages ?? [],
loading: props.smart?.loading ?? false,
error: props.smart?.error ?? null,
showSidebar: props.smart?.showSidebar ?? true,
askFields: props.smart?.askFields ?? [],
askValues: props.smart?.askValues,
}));
</script>
<style scoped>
.kb-search {
width: 100%;
}
/* 统一版心容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 24px 0px;
}
/* 搜索区域(白底,满屏) */
.search-section {
width: 100%;
background: #ffffff;
}
/* 结果区域(灰底,满屏) */
.results-section {
width: 100%;
background: #f5f7fa;
}
.mode-switch {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
}
/* 简单搜索输入容器,宽度约 70% */
.input-wrap-70 {
width: 70%;
margin: 0 auto;
}
/* 通用内容包裹(居中,不额外限制宽度) */
.content-wrap {
width: 100%;
}
.loading-state,
.error-state,
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.results-list {
width: 100%;
}
@media (max-width: 992px) {
.input-wrap-70 {
width: 100%;
}
}
</style>
<template>
<div class="default-recommend">
<div class="left">
<div class="card">
<div class="card-header">
<span class="bar" :style="{ background: primaryColor }"></span>
<span class="card-title-text">热门关键词</span>
</div>
<div class="card-body">
<div class="keywords">
<a-tag
v-for="kw in keywords"
:key="kw"
color="blue"
size="medium"
class="kw"
>
{{ kw }}
</a-tag>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="bar" :style="{ background: primaryColor }"></span>
<span class="card-title-text">热门文件</span>
</div>
<div class="card-body">
<div class="file-list">
<div class="file-item" v-for="(f, idx) in files" :key="idx">
<a-typography-text ellipsis>{{ f.name }}</a-typography-text>
<a-typography-text type="secondary" size="small">{{
f.date
}}</a-typography-text>
</div>
</div>
</div>
</div>
</div>
<div class="right">
<div class="card">
<div class="card-header">
<span class="bar" :style="{ background: primaryColor }"></span>
<span class="card-title-text">为你推荐</span>
</div>
<div class="card-body">
<div class="rec-list">
<div class="rec-item" v-for="it in items" :key="it.id || it.title">
<div class="rec-meta">
<div class="rec-title">{{ it.title }}</div>
<div class="rec-desc">{{ it.description }}</div>
</div>
<div class="rec-extra">
<a-tag v-if="it.tags && it.tags.length" size="small">{{
it.tags[0]
}}</a-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { SearchResult } from "../../types";
const props = defineProps<{
keywords: string[];
files: { name: string; date?: string }[];
items: SearchResult[];
primaryColor?: string;
}>();
const primaryColor = props.primaryColor || "#165DFF";
</script>
<style scoped>
.default-recommend {
display: flex;
gap: 16px;
}
.left {
flex: 1; /* 1/5 */
display: flex;
flex-direction: column;
gap: 16px;
}
.right {
flex: 4; /* 4/5 */
}
/* 自定义卡片(参考 Arco Card) */
.card {
background: #ffffff;
border: 1px solid #e5e6eb;
border-radius: 8px;
overflow: visible; /* 让自定义条可见 */
}
.card + .card {
margin-top: 0;
}
.card-header {
position: relative;
display: flex;
align-items: center;
padding: 12px 16px 12px 20px; /* 左侧稍加内边距,给标题留空间 */
border-bottom: 1px solid #e5e6eb;
line-height: 20px;
}
/* 左侧主题色竖条:4px 宽,高度与标题一致,与卡片左边对齐 */
.card-header .bar {
position: absolute;
left: -1px; /* 与卡片边框对齐,避免被裁剪 */
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 1em; /* 与标题文字等高 */
border-radius: 2px;
}
.card-title-text {
font-weight: 600;
color: #1d2129;
}
.card-body {
padding: 12px 16px;
}
.keywords {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.kw {
margin: 0;
}
.file-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.rec-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.rec-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.rec-meta {
flex: 1;
}
.rec-title {
font-weight: 600;
color: #1d2129;
margin-bottom: 4px;
}
.rec-desc {
color: #86909c;
font-size: 13px;
}
.rec-extra {
flex: none;
}
@media (max-width: 992px) {
.default-recommend {
flex-direction: column;
}
.left,
.right {
flex: none;
}
}
</style>
<template>
<div class="default-result-item">
<a-typography-title
:heading="5"
class="title"
:ellipsis="{ tooltip: true, rows: 2 }"
>
{{ getFieldValue(item, "title") || "无标题" }}
</a-typography-title>
<a-divider />
<a-space direction="vertical" size="small" style="width: 100%">
<template v-for="field in config.display.fields" :key="field.key">
<div
v-if="field.key !== 'title' && getFieldValue(item, field.key)"
class="field-item"
>
<a-typography-text strong style="width: 80px; display: inline-block">
{{ field.label }}:
</a-typography-text>
<span class="field-value">
{{ formatField(field, item) }}
</span>
</div>
</template>
</a-space>
<a-divider />
<div class="meta">
<a-tag v-if="item.tags && item.tags.length" color="blue" size="small">
{{ item.tags.join(", ") }}
</a-tag>
<a-typography-text type="secondary" style="margin-left: 8px">
{{ formatDate(item.date) }}
</a-typography-text>
</div>
</div>
</template>
<script setup lang="ts">
import { h } from "vue";
import type { SearchConfig, FieldConfig } from "../../types";
import dayjs from "dayjs";
interface Props {
item: any;
config: SearchConfig;
}
const props = defineProps<Props>();
const getFieldValue = (item: any, key: string) => {
return item[key];
};
const formatField = (field: FieldConfig, item: any) => {
const value = getFieldValue(item, field.key);
if (field.formatter) {
return field.formatter(value, item);
}
switch (field.type) {
case "date":
return dayjs(value).format("YYYY-MM-DD HH:mm");
case "tag":
return Array.isArray(value) ? value.join(", ") : value;
default:
return value;
}
};
const formatDate = (date: string | Date | undefined) => {
if (!date) return "";
return dayjs(date).format("YYYY-MM-DD");
};
</script>
<style scoped>
.default-result-item {
padding: 16px 0;
}
.title {
margin-bottom: 8px !important;
}
.field-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
}
.field-value {
flex: 1;
color: var(--color-text-2);
line-height: 1.5;
}
.meta {
display: flex;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--color-border);
}
@media (max-width: 768px) {
.field-item {
flex-direction: column;
}
.field-value {
margin-top: 4px;
}
}
</style>
<template>
<div class="search-results">
<a-list
v-if="config.display.layout === 'list'"
:data="results"
:loading="loading"
:pagination="pagination"
@change="handlePaginationChange"
>
<template #header>
<div class="results-header">
<a-typography-title :heading="6">搜索结果 ({{ total }})</a-typography-title>
<a-select v-model:value="layout" :options="layoutOptions" style="width: 120px;" />
</div>
</template>
<template #renderItem="{ item }">
<a-list-item>
<template #extra>
<a-button type="text" @click="handleDetail(item)">详情</a-button>
</template>
<div class="result-item">
<slot name="result-item" :item="item">
<DefaultResultItem :item="item" :config="config" />
</slot>
</div>
</a-list-item>
</template>
</a-list>
<a-card
v-else-if="config.display.layout === 'card'"
:title="`搜索结果 (${total})`"
:loading="loading"
:bordered="false"
v-for="item in results"
:key="item.id"
style="margin-bottom: 16px;"
>
<template #extra>
<a-button type="text" @click="handleDetail(item)">详情</a-button>
</template>
<slot name="result-item" :item="item">
<DefaultResultItem :item="item" :config="config" />
</slot>
</a-card>
<a-empty v-else-if="!loading && results.length === 0" description="暂无结果" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { SearchResult, SearchConfig } from '../../types'
import DefaultResultItem from './DefaultResultItem.vue'
const props = defineProps<{
config: SearchConfig
results: SearchResult[]
total: number
loading: boolean
}>()
const emit = defineEmits<{
'result-click': [item: SearchResult]
'pagination-change': [page: number]
}>()
const layout = ref(props.config.display.layout || 'list')
const pagination = computed(() => ({
current: props.results.length ? 1 : 0,
total: props.total,
pageSize: props.config.display.perPage || 10,
showSizeChanger: false,
showQuickJumper: true
}))
const layoutOptions = [
{ label: '列表', value: 'list' },
{ label: '卡片', value: 'card' }
]
const handlePaginationChange = (pagination: any) => {
emit('pagination-change', pagination.current)
}
const handleDetail = (item: SearchResult) => {
emit('result-click', item)
}
</script>
<style scoped>
.search-results {
flex: 1;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-item {
width: 100%;
}
@media (max-width: 768px) {
.results-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.results-header .arco-select {
align-self: flex-end;
}
}
</style>
<template>
<div v-if="recommend && recommend.length > 0" class="recommend-grid">
<div
v-for="chunk in recommend"
:key="chunk.chunkId"
class="recommend-chunk"
:style="{ gridColumn: `span ${chunk.col}` }"
>
<div class="chunk-header">
<span
class="chunk-bar"
:style="{ background: 'var(--search-primary-color)' }"
></span>
<span class="chunk-title">{{ chunk.title }}</span>
</div>
<div class="chunk-body">
<slot name="recommend-item" :chunk="chunk" :items="chunk.items">
<div
v-for="item in chunk.items"
:key="item.id"
class="recommend-item"
@click="handleItemClick(item)"
>
<div class="item-title" v-html="highlightTitle(item)"></div>
<div v-if="item.desc" class="item-desc">{{ item.desc }}</div>
<div v-if="item.tags && item.tags.length" class="item-tags">
<span v-for="tag in item.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</div>
</slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { RecommendChunk, SearchResultItem } from "./types";
interface Props {
recommend?: RecommendChunk[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
select: [item: SearchResultItem];
}>();
// col 直接对应 24 栅格系统的列数,无需映射
const highlightTitle = (item: SearchResultItem): string => {
let title = item.title || "";
if (item.highlightKws && item.highlightKws.length > 0) {
item.highlightKws.forEach((kw) => {
const regex = new RegExp(`(${kw})`, "gi");
title = title.replace(
regex,
`<mark style="background: var(--search-highlight-bg, #e8f3ff); color: var(--search-highlight-color, var(--search-primary-color));">$1</mark>`
);
});
}
return title;
};
const handleItemClick = (item: SearchResultItem) => {
emit("select", item);
};
</script>
<style scoped>
.recommend-grid {
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: var(--search-gap, 16px);
margin-bottom: var(--search-gap, 16px);
}
.recommend-chunk {
background: var(--search-card-bg, #ffffff);
border: 1px solid var(--search-card-border, #e5e6eb);
border-radius: var(--search-border-radius, 8px);
overflow: hidden;
}
.chunk-header {
position: relative;
display: flex;
align-items: center;
padding: 12px 16px 12px 20px;
border-bottom: 1px solid var(--search-card-border, #e5e6eb);
}
.chunk-bar {
position: absolute;
left: -1px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 1em;
border-radius: 2px;
}
.chunk-title {
font-weight: 600;
color: var(--search-text-primary, #1d2129);
}
.chunk-body {
padding: 12px 16px;
}
.recommend-item {
padding: 8px 0;
cursor: pointer;
transition: background-color 0.2s;
}
.recommend-item:hover {
background-color: var(--search-highlight-bg, #f2f3f5);
border-radius: 4px;
padding: 8px 12px;
margin: 0 -12px;
}
.recommend-item + .recommend-item {
border-top: 1px solid var(--search-card-border, #e5e6eb);
}
.item-title {
font-weight: 500;
color: var(--search-text-primary, #1d2129);
margin-bottom: 4px;
line-height: 1.5;
}
.item-desc {
font-size: 13px;
color: var(--search-text-secondary, #86909c);
margin-bottom: 4px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.tag {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
color: var(--search-text-secondary, #86909c);
background: var(--search-highlight-bg, #f2f3f5);
border-radius: 4px;
}
</style>
<template>
<div class="result-card" @click="handleClick">
<div class="card-header">
<h3 class="card-title" v-html="highlightTitle"></h3>
<div v-if="hasSubtitle" class="card-subtitle">{{ subtitle }}</div>
</div>
<div v-if="hasDesc" class="card-desc" v-html="highlightDesc"></div>
<div v-if="hasMeta" class="card-meta">
<span v-if="author" class="meta-item">
<span class="meta-label">作者:</span>
<span class="meta-value">{{ author }}</span>
</span>
<span v-if="publishTime" class="meta-item">
<span class="meta-label">发布时间:</span>
<span class="meta-value">{{ formatTime }}</span>
</span>
</div>
<div v-if="hasTags" class="card-tags">
<span v-for="tag in tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { SearchResultItem } from "./types";
import dayjs from "dayjs";
interface Props {
item: SearchResultItem;
}
const props = defineProps<Props>();
const emit = defineEmits<{
click: [item: SearchResultItem];
}>();
// 字段降级处理,确保不报错
const title = computed(() => props.item.title || "");
const subtitle = computed(() => props.item.subtitle || "");
const desc = computed(() => props.item.desc || "");
const tags = computed(() => props.item.tags || []);
const author = computed(() => props.item.author || null);
const publishTime = computed(() => props.item.publishTime || null);
const highlightKws = computed(() => props.item.highlightKws || []);
// 判断是否有内容
const hasSubtitle = computed(() => !!subtitle.value);
const hasDesc = computed(() => !!desc.value);
const hasTags = computed(() => tags.value.length > 0);
const hasMeta = computed(() => !!author.value || !!publishTime.value);
// 高亮标题
const highlightTitle = computed(() => {
let text = title.value;
if (highlightKws.value.length > 0) {
highlightKws.value.forEach((kw) => {
const regex = new RegExp(`(${kw})`, "gi");
text = text.replace(
regex,
`<mark style="background: var(--search-highlight-bg, #e8f3ff); color: var(--search-highlight-color, var(--search-primary-color));">$1</mark>`
);
});
}
return text;
});
// 高亮描述
const highlightDesc = computed(() => {
let text = desc.value;
if (highlightKws.value.length > 0) {
highlightKws.value.forEach((kw) => {
const regex = new RegExp(`(${kw})`, "gi");
text = text.replace(
regex,
`<mark style="background: var(--search-highlight-bg, #e8f3ff); color: var(--search-highlight-color, var(--search-primary-color));">$1</mark>`
);
});
}
return text;
});
// 格式化时间
const formatTime = computed(() => {
if (!publishTime.value) return "";
try {
return dayjs(publishTime.value).format("YYYY-MM-DD HH:mm");
} catch {
return String(publishTime.value);
}
});
const handleClick = () => {
emit("click", props.item);
};
</script>
<style scoped>
.result-card {
padding: var(--search-card-padding, 16px);
background: var(--search-card-bg, #ffffff);
border: 1px solid var(--search-card-border, #e5e6eb);
border-radius: var(--search-border-radius, 8px);
cursor: pointer;
transition: all 0.2s;
margin-bottom: var(--search-gap, 16px);
}
.result-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: var(--search-primary-color, #165dff);
}
.card-header {
margin-bottom: 12px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--search-text-primary, #1d2129);
margin: 0 0 4px 0;
line-height: 1.5;
}
.card-subtitle {
font-size: 14px;
color: var(--search-text-secondary, #86909c);
margin-top: 4px;
}
.card-desc {
font-size: 14px;
color: var(--search-text-secondary, #86909c);
line-height: 1.6;
margin-bottom: 12px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.card-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 13px;
color: var(--search-text-secondary, #86909c);
}
.meta-item {
display: flex;
align-items: center;
}
.meta-label {
color: var(--search-text-tertiary, #c9cdd4);
margin-right: 4px;
}
.meta-value {
color: var(--search-text-secondary, #86909c);
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
display: inline-block;
padding: 4px 10px;
font-size: 12px;
color: var(--search-primary-color, #165dff);
background: var(--search-highlight-bg, #e8f3ff);
border-radius: 4px;
border: 1px solid transparent;
}
.tag:hover {
border-color: var(--search-primary-color, #165dff);
}
</style>
<template>
<div class="search-input-wrapper">
<a-input-search
:model-value="modelValue"
:placeholder="placeholder"
:loading="loading"
:disabled="loading"
enter-button
allow-clear
size="large"
@input="handleInput"
@search="handleSearch"
@press-enter="handleSearch"
class="search-input"
>
</a-input-search>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
interface Props {
modelValue: string;
loading?: boolean;
error?: string | null;
placeholder?: string;
debounce?: number;
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
error: null,
placeholder: "输入关键词搜索...",
debounce: 300,
});
const emit = defineEmits<{
"update:modelValue": [value: string];
search: [keyword: string];
}>();
const inputValue = ref(props.modelValue);
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
// 防抖处理输入
const debouncedUpdate = (value: string) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
emit("update:modelValue", value);
}, props.debounce);
};
const handleInput = (value: string) => {
inputValue.value = value;
debouncedUpdate(value);
};
const handleSearch = () => {
const keyword = inputValue.value.trim();
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
if (keyword) {
emit("update:modelValue", keyword);
emit("search", keyword);
}
};
// 监听外部 modelValue 变化
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== inputValue.value) {
inputValue.value = newValue;
}
}
);
</script>
<style scoped>
.search-input-wrapper {
width: 100%;
}
.search-input {
width: 100%;
}
.error-message {
margin-top: 8px;
color: var(--search-error-color, #f53f3f);
font-size: 12px;
}
</style>
/**
* 普通搜索组件可选 Hook
* 业务方可以使用这个 hook 来管理搜索状态,也可以自己实现
*/
import { ref, computed } from 'vue'
export interface UseSearchOptions<T = any> {
/**
* 搜索请求函数
* @param keyword 搜索关键词
* @returns Promise<{ data: T[], total?: number }>
*/
searchFn: (keyword: string, extra?: Record<string, any>) => Promise<{ data: T[], total?: number }>
/**
* 初始关键词
*/
initialKeyword?: string
/**
* 每页数量
*/
pageSize?: number
}
/**
* 搜索 Hook 返回值
*/
export interface UseSearchReturn<T = any> {
/** 搜索关键词 */
keyword: ReturnType<typeof ref<string>>
/** 搜索结果列表 */
results: ReturnType<typeof ref<T[]>>
/** 加载状态 */
loading: ReturnType<typeof ref<boolean>>
/** 错误信息 */
error: ReturnType<typeof ref<string | null>>
/** 分页信息 */
pagination: ReturnType<typeof ref<{
current: number
pageSize: number
total: number
hasMore: boolean
}>>
/** 执行搜索 */
onSearch: (keyword: string, extra?: Record<string, any>) => Promise<void>
/** 加载更多 */
loadMore: () => Promise<void>
/** 重置搜索 */
reset: () => void
}
/**
* 搜索 Hook
*
* @example
* ```ts
* 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 }
* }
* })
* ```
*/
export function useSearch<T = any>(options: UseSearchOptions<T>): UseSearchReturn<T> {
const { searchFn, initialKeyword = '', pageSize = 10 } = options
const keyword = ref<string>(initialKeyword)
const results = ref<T[]>([]) as ReturnType<typeof ref<T[]>>
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const pagination = ref({
current: 1,
pageSize,
total: 0,
hasMore: false
})
const onSearch = async (searchKeyword: string, extra?: Record<string, any>) => {
if (!searchKeyword.trim()) {
results.value = []
pagination.value = {
current: 1,
pageSize,
total: 0,
hasMore: false
}
return
}
keyword.value = searchKeyword
loading.value = true
error.value = null
try {
const response = await searchFn(searchKeyword, extra)
const data: T[] = Array.isArray(response.data) ? response.data : []
results.value = data
pagination.value = {
current: 1,
pageSize,
total: response.total || data.length,
hasMore: response.total ? data.length < response.total : false
}
} catch (err: any) {
error.value = err.message || '搜索失败,请稍后重试'
results.value = []
} finally {
loading.value = false
}
}
const loadMore = async () => {
if (loading.value || !pagination.value.hasMore) return
loading.value = true
error.value = null
try {
const nextPage = pagination.value.current + 1
// 这里需要业务方提供支持分页的搜索函数
// 或者通过 extra 参数传递分页信息
const response = await searchFn(keyword.value, {
page: nextPage,
pageSize: pagination.value.pageSize
})
const newData: T[] = Array.isArray(response.data) ? response.data : []
const currentResults = Array.isArray(results.value) ? results.value : []
results.value = [...currentResults, ...newData] as T[]
pagination.value = {
current: nextPage,
pageSize: pagination.value.pageSize,
total: response.total || results.value.length,
hasMore: response.total ? results.value.length < response.total : false
}
} catch (err: any) {
error.value = err.message || '加载失败,请稍后重试'
} finally {
loading.value = false
}
}
const reset = () => {
keyword.value = ''
results.value = []
error.value = null
pagination.value = {
current: 1,
pageSize,
total: 0,
hasMore: false
}
}
return {
keyword: keyword as ReturnType<typeof ref<string>>,
results: results as ReturnType<typeof ref<T[]>>,
loading: loading as ReturnType<typeof ref<boolean>>,
error: error as ReturnType<typeof ref<string | null>>,
pagination: pagination as ReturnType<typeof ref<{
current: number
pageSize: number
total: number
hasMore: boolean
}>>,
onSearch,
loadMore,
reset
}
}
<template>
<div class="ordinary-search">
<!-- 搜索输入区域 -->
<div class="search-section">
<div class="search-row">
<div class="search-input-col">
<SearchInput
:model-value="keyword || ''"
:loading="loading"
:error="error"
:placeholder="placeholder"
:debounce="debounce || 300"
@update:model-value="handleKeywordUpdate"
@search="handleSearch"
/>
</div>
<div class="search-actions">
<a-button
type="primary"
size="large"
:loading="loading"
@click="handleSearchClick"
>
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button
style="margin-left: 12px"
size="large"
@click="handleReset"
:disabled="loading"
>
<template #icon><icon-refresh /></template>
重置
</a-button>
</div>
</div>
<!-- 条件控件区域插槽 -->
<div
v-if="hasConditionsSlot || (conditions && conditions.length > 0)"
class="conditions-section"
>
<slot name="conditions" :conditions="conditions">
<!-- 默认条件渲染(可选) -->
<div
v-if="conditions && conditions.length > 0"
class="default-conditions"
>
<!-- 业务方可以通过插槽自定义,这里只做兜底展示 -->
</div>
</slot>
</div>
</div>
<!-- 推荐区域 -->
<div v-if="showRecommend" class="recommend-section">
<slot name="recommend" :recommend="recommend">
<RecommendGrid :recommend="recommend" @select="handleSelect">
<template #recommend-item="{ chunk, items }">
<slot name="recommend-item" :chunk="chunk" :items="items"></slot>
</template>
</RecommendGrid>
</slot>
</div>
<!-- 搜索结果区域 -->
<div v-if="showResults" class="results-section">
<div v-if="loading && results.length === 0" class="loading-state">
<a-spin />
</div>
<div v-else-if="error && results.length === 0" class="error-state">
<a-alert type="error" :message="error" />
</div>
<div v-else-if="results.length === 0" class="empty-state">
<a-empty description="暂无搜索结果" />
</div>
<div v-else class="results-list">
<slot name="result" :item="item" v-for="item in results" :key="item.id">
<ResultCard :item="item" @click="handleSelect" />
</slot>
</div>
<!-- 加载更多 -->
<div v-if="showLoadMore" class="load-more">
<a-button
:loading="loading"
@click="handleLoadMore"
type="primary"
size="large"
>
加载更多
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { IconSearch, IconRefresh } from "@arco-design/web-vue/es/icon";
import SearchInput from "./SearchInput.vue";
import RecommendGrid from "./RecommendGrid.vue";
import ResultCard from "./ResultCard.vue";
import "./style.css";
// 定义 props
const props = withDefaults(
defineProps<{
keyword?: string;
loading?: boolean;
error?: string | null;
results?: any[];
pagination?: any;
recommend?: any[];
conditions?: any[];
debounce?: number;
placeholder?: string;
}>(),
{
keyword: "",
loading: false,
error: null,
results: () => [],
pagination: undefined,
recommend: () => [],
conditions: () => [],
debounce: 300,
placeholder: "输入关键词搜索...",
}
);
// 定义 emits
const emit = defineEmits<{
(e: "update:keyword", value: string): void;
(e: "search", payload: { keyword: string; extra: any }): void;
(e: "loadMore"): void;
(e: "select", item: any): void;
}>();
// 计算属性
const showRecommend = computed(() => {
return (
props.recommend && props.recommend.length > 0 && !props.keyword?.trim()
);
});
const showResults = computed(() => {
return !!props.keyword?.trim() || props.results.length > 0;
});
const showLoadMore = computed(() => {
if (!props.pagination) return false;
return props.pagination.hasMore !== false && props.results.length > 0;
});
const hasConditionsSlot = computed(() => {
// 通过插槽检查,这里简化处理,实际可以通过 useSlots 检查
return false;
});
// 事件处理
const handleKeywordUpdate = (value: string) => {
emit("update:keyword", value);
};
const handleSearch = (keyword: string) => {
emit("search", { keyword, extra: {} });
};
// 点击按钮触发搜索(使用当前受控 keyword)
const handleSearchClick = () => {
const kw = (props as any).keyword?.trim?.() || "";
emit("search", { keyword: kw, extra: {} });
};
// 重置:清空关键字并通知父级,可由父级清空结果/恢复推荐
const handleReset = () => {
emit("update:keyword", "");
emit("search", { keyword: "", extra: {} });
};
const handleLoadMore = () => {
emit("loadMore");
};
const handleSelect = (item: any) => {
emit("select", item);
};
</script>
<style scoped>
.ordinary-search {
width: 100%;
}
.search-section {
margin-bottom: var(--search-gap, 16px);
}
.search-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.search-input-col {
flex: 1;
}
.search-actions {
display: flex;
align-items: center;
}
.conditions-section {
margin-top: 12px;
}
.recommend-section {
margin-bottom: var(--search-gap, 16px);
}
.results-section {
width: 100%;
}
.loading-state,
.error-state,
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.results-list {
width: 100%;
}
.load-more {
display: flex;
justify-content: center;
padding: 24px 0;
}
</style>
/**
* 普通搜索组件主题变量
* 业务方可通过覆盖这些变量来自定义主题
*/
:root {
/* 主题色 */
--search-primary-color: #165DFF;
/* 圆角 */
--search-border-radius: 8px;
/* 间距 */
--search-gap: 16px;
/* 输入框 */
--search-input-height: 40px;
--search-input-border-color: #e5e6eb;
--search-input-focus-border-color: var(--search-primary-color);
/* 卡片 */
--search-card-bg: #ffffff;
--search-card-border: #e5e6eb;
--search-card-padding: 16px;
/* 文字颜色 */
--search-text-primary: #1d2129;
--search-text-secondary: #86909c;
--search-text-tertiary: #c9cdd4;
/* 高亮色 */
--search-highlight-bg: #e8f3ff;
--search-highlight-color: var(--search-primary-color);
/* 按钮 */
--search-btn-primary-bg: var(--search-primary-color);
--search-btn-primary-color: #ffffff;
--search-btn-hover-bg: #0e42d2;
/* 错误状态 */
--search-error-color: #f53f3f;
}
/**
* 普通搜索组件类型定义
*/
// 搜索结果项(最小字段要求:id, title)
export 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;
}
// 分页信息
export interface PaginationInfo {
current?: number;
pageSize?: number;
total?: number;
hasMore?: boolean;
}
// 推荐块项
export interface RecommendChunk {
chunkId: string | number;
title: string;
col: number; // 栅格列数,直接对应 24 栅格系统 (1-24)
items: SearchResultItem[];
}
// 搜索条件配置
export interface SearchCondition {
key: string;
label: string;
type: 'select' | 'input' | 'date' | 'checkbox' | 'radio';
options?: Array<{ label: string; value: any }>;
}
// 搜索事件参数
export interface SearchEventParams {
keyword: string;
extra?: Record<string, any>;
}
// 组件 Props
export interface SimpleSearchProps {
keyword?: string;
loading: boolean;
error?: string | null;
results: SearchResultItem[];
pagination?: PaginationInfo;
recommend?: RecommendChunk[];
conditions?: SearchCondition[];
debounce?: number;
placeholder?: string;
}
// 组件 Emits
export interface SimpleSearchEmits {
(e: 'update:keyword', value: string): void;
(e: 'search', params: SearchEventParams): void;
(e: 'loadMore'): void;
(e: 'select', item: SearchResultItem): void;
}
<template>
<div class="smart-search">
<div class="layout">
<!-- 会话历史侧边栏 -->
<aside v-if="showSidebar" class="sidebar">
<div class="sidebar-header">
<span>会话历史</span>
<a-button size="small" type="primary" @click="handleNewSession">
<template #icon><icon-message /></template>
新建
</a-button>
</div>
<div class="session-list">
<div
v-for="s in sessions"
:key="s.id"
class="session-item"
:class="{ active: s.id === currentSessionId }"
@click="handleSwitchSession(s.id)"
title="点击恢复会话"
>
<div class="title">{{ s.title || "未命名会话" }}</div>
<div class="time">{{ formatTime(s.createdAt) }}</div>
</div>
</div>
</aside>
<!-- 聊天主区域 -->
<section class="chat">
<div class="chat-messages" ref="listRef">
<div
v-for="msg in messages"
:key="msg.id"
class="message"
:class="msg.role"
>
<div class="avatar">
<span v-if="msg.role === 'user'">U</span>
<span v-else>AI</span>
</div>
<div class="bubble">
<pre class="content">{{ msg.content }}</pre>
</div>
</div>
<div v-if="loading" class="message assistant typing">
<div class="avatar"><span>AI</span></div>
<div class="bubble">
<span class="dot" />
<span class="dot" />
<span class="dot" />
</div>
</div>
<div v-if="error" class="message assistant error">
<div class="avatar"><span>AI</span></div>
<div class="bubble">
<pre class="content">{{ error }}</pre>
</div>
</div>
</div>
<div class="composer">
<a-textarea
v-model="inputMessage"
placeholder="输入问题,Shift+Enter 换行,Enter 发送"
:auto-size="{ minRows: 2, maxRows: 6 }"
:disabled="loading"
@keydown.enter.prevent.exact="onEnter"
@keydown.enter.shift.exact.stop
/>
<div class="actions">
<a-button type="primary" :loading="loading" @click="handleSend">
<template #icon><icon-send /></template>
发送
</a-button>
<a-button status="warning" :disabled="!loading" @click="handleStop">
<template #icon><icon-pause /></template>
停止
</a-button>
<a-button
status="danger"
:disabled="!messages || messages.length === 0"
@click="handleClearSession"
>
<template #icon><icon-delete /></template>
清空
</a-button>
</div>
</div>
</section>
<!-- 右侧参数区域(可选) -->
<aside v-if="askFields && askFields.length" class="ask-panel">
<div class="ask-header">提问参数</div>
<a-form layout="vertical" :model="askForm">
<a-form-item
v-for="field in askFields"
:key="field.key"
:label="field.label"
:required="field.required"
>
<a-input
v-if="field.type === 'text'"
v-model="askForm[field.key]"
:placeholder="getPlaceholder(field) || `请输入${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-textarea
v-else-if="field.type === 'textarea'"
v-model="askForm[field.key]"
:placeholder="getPlaceholder(field) || `请输入${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-input-number
v-else-if="field.type === 'number'"
v-model="askForm[field.key]"
style="width: 100%"
:placeholder="getPlaceholder(field) || `请输入${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-select
v-else-if="field.type === 'select'"
v-model="askForm[field.key]"
allow-clear
style="width: 100%"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>
<a-option
v-for="opt in field.options"
:key="opt.value"
:value="opt.value"
>{{ opt.label }}</a-option
>
</a-select>
<a-select
v-else-if="field.type === 'multi-select'"
v-model="askForm[field.key]"
mode="multiple"
allow-clear
style="width: 100%"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>
<a-option
v-for="opt in field.options"
:key="opt.value"
:value="opt.value"
>{{ opt.label }}</a-option
>
</a-select>
<a-radio-group
v-else-if="field.type === 'radio'"
v-model="askForm[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>
<a-radio
v-for="opt in field.options"
:key="opt.value"
:value="opt.value"
>{{ opt.label }}</a-radio
>
</a-radio-group>
<a-checkbox
v-else-if="field.type === 'checkbox'"
v-model="askForm[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
>{{ field.label }}</a-checkbox
>
<a-switch
v-else-if="field.type === 'switch'"
v-model="askForm[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-slider
v-else-if="field.type === 'slider'"
v-model="askForm[field.key]"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-date-picker
v-else-if="field.type === 'date'"
v-model="askForm[field.key]"
style="width: 100%"
:placeholder="getPlaceholder(field) || `请选择${field.label}`"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-range-picker
v-else-if="field.type === 'date-range'"
v-model="askForm[field.key]"
style="width: 100%"
:placeholder="
Array.isArray(field.placeholder)
? field.placeholder
: field.placeholder
? [field.placeholder, field.placeholder]
: ['开始日期', '结束日期']
"
:disabled="field.disabled || loading"
v-bind="field.props || {}"
/>
<a-upload
v-else-if="field.type === 'file' && field.upload"
:action="field.upload.action"
:headers="field.upload.headers"
:name="field.upload.fieldName || 'file'"
:data="field.upload.data"
:multiple="field.upload.multiple"
:accept="field.upload.accept"
:limit="field.upload.limit"
:auto-upload="true"
:disabled="field.disabled || loading"
@success="onUploadSuccessWrapped(field.key)"
/>
</a-form-item>
</a-form>
</aside>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch, onMounted } from "vue";
import {
IconSend,
IconPause,
IconDelete,
IconMessage,
} from "@arco-design/web-vue/es/icon";
import type {
SmartSearchProps,
SmartSearchEmits,
SmartAskField,
} from "./types";
const props = withDefaults(defineProps<SmartSearchProps>(), {
sessions: () => [],
currentSessionId: undefined,
messages: () => [],
loading: false,
error: null,
showSidebar: true,
askFields: () => [],
askValues: undefined,
});
const emit = defineEmits<SmartSearchEmits>();
const inputMessage = ref("");
const listRef = ref<HTMLElement | null>(null);
const askForm = ref<Record<string, any>>({});
// 格式化时间
const formatTime = (ts: number | Date): string => {
if (!ts) return "";
const date = typeof ts === "number" ? new Date(ts) : ts;
return date.toLocaleString();
};
// 滚动到底部
const scrollToBottom = async () => {
await nextTick();
const el = listRef.value;
if (el) {
el.scrollTop = el.scrollHeight;
}
};
// 监听消息变化,自动滚动
watch(
() => props.messages,
() => {
scrollToBottom();
},
{ deep: true }
);
watch(
() => props.loading,
() => {
if (props.loading) {
scrollToBottom();
}
}
);
// 事件处理
const handleSend = () => {
const text = inputMessage.value.trim();
if (!text || props.loading) return;
emit("send", text);
// 同时透出带参数的发送(不影响兼容)
emit("send-with", { message: text, params: { ...(askForm.value || {}) } });
inputMessage.value = "";
};
const onEnter = () => {
if (!props.loading) {
handleSend();
}
};
const handleStop = () => {
emit("stop");
};
const handleNewSession = () => {
emit("new-session");
};
const handleSwitchSession = (sessionId: string | number) => {
emit("switch-session", sessionId);
};
const handleClearSession = () => {
if (props.currentSessionId) {
emit("clear-session", props.currentSessionId);
}
};
onMounted(() => {
scrollToBottom();
initAskForm();
});
// 初始化右侧参数表单
const initAskForm = () => {
const data: Record<string, any> = {};
(props.askFields || []).forEach((f) => {
if (props.askValues && f.key in (props.askValues || {})) {
data[f.key] = (props.askValues as any)[f.key];
} else if (f.defaultValue !== undefined) {
data[f.key] = f.defaultValue;
} else {
switch (f.type) {
case "multi-select":
case "date-range":
data[f.key] = [];
break;
case "switch":
case "checkbox":
data[f.key] = false;
break;
case "number":
data[f.key] = undefined;
break;
default:
data[f.key] = "";
}
}
});
askForm.value = data;
};
watch(
() => props.askFields,
() => initAskForm(),
{ deep: true }
);
watch(
() => props.askValues,
(val) => {
if (val && typeof val === "object") {
askForm.value = { ...askForm.value, ...(val as any) };
}
},
{ deep: true }
);
watch(
() => askForm.value,
(val) => emit("update:askValues", { ...(val || {}) }),
{ deep: true }
);
const getPlaceholder = (field: SmartAskField): string | undefined => {
return typeof field.placeholder === "string" ? field.placeholder : undefined;
};
const onUploadSuccess = (key: string, file: any) => {
// 以文件响应为值,业务方可在 send-with 中接收
const list = Array.isArray(askForm.value[key]) ? askForm.value[key] : [];
askForm.value[key] = [...list, file];
};
// 适配 Arco Upload 的 success 事件签名
const onUploadSuccessWrapped = (key: string) => {
return (_fileItem: any) => {
onUploadSuccess(key, _fileItem);
};
};
</script>
<style scoped>
.smart-search {
height: 500px;
display: flex;
flex-direction: column;
}
.layout {
display: flex;
height: 100%;
gap: 12px;
}
/* 侧边栏 */
.sidebar {
width: 220px;
background: #fff;
border: 1px solid #f2f3f5;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f2f3f5;
font-weight: 600;
}
.session-list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.session-item {
border: 1px solid #f2f3f5;
border-radius: 6px;
padding: 8px;
cursor: pointer;
background: #fff;
transition: box-shadow 0.2s, border-color 0.2s;
}
.session-item:hover {
border-color: #e9ebf0;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.04);
}
.session-item.active {
border-color: var(--color-primary-light);
box-shadow: 0 0 0 2px var(--color-primary-light);
}
.session-item .title {
font-weight: 600;
color: #1d2129;
}
.session-item .time {
color: #86909c;
font-size: 12px;
margin-top: 4px;
}
.chat {
flex: 1;
display: flex;
flex-direction: column;
}
/* 右侧参数面板 */
.ask-panel {
width: 280px;
background: #fff;
border: 1px solid #f2f3f5;
border-radius: 8px;
padding: 12px;
overflow: auto;
}
.ask-header {
font-weight: 600;
margin-bottom: 8px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
background: var(--color-fill-2);
border-radius: 8px;
margin-bottom: 12px;
}
.message {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.message.user {
flex-direction: row-reverse;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: #fff;
}
.message.user .avatar {
background: var(--color-primary);
}
.message.assistant .avatar {
background: linear-gradient(135deg, #6fb1ff 0%, #79ffe1 100%);
color: #1d2129;
}
.bubble {
max-width: 70%;
white-space: pre-wrap;
word-break: break-word;
background: #fff;
padding: 8px 12px;
border-radius: 8px;
box-shadow: var(--shadow-1);
}
.message.user .bubble {
background: var(--color-primary-light);
}
.message.assistant.error .bubble {
background: #fee2e2;
color: #dc2626;
}
.typing .bubble {
display: inline-flex;
gap: 6px;
align-items: center;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #999;
animation: blink 1.2s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%,
80%,
100% {
opacity: 0.2;
}
40% {
opacity: 1;
}
}
.composer {
display: flex;
flex-direction: column;
gap: 8px;
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
@media (max-width: 992px) {
.layout {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.bubble {
max-width: 85%;
}
}
</style>
/**
* 智能搜索组件类型定义
*/
// 聊天消息
export interface ChatMessage {
id: string | number;
role: 'user' | 'assistant';
content: string;
}
// 会话信息
export interface ChatSession {
id: string | number;
title: string;
createdAt: number | Date;
}
// 组件 Props
export interface SmartSearchProps {
/** 会话列表 */
sessions?: ChatSession[];
/** 当前会话ID */
currentSessionId?: string | number;
/** 当前会话的消息列表 */
messages?: ChatMessage[];
/** 加载状态 */
loading?: boolean;
/** 错误信息 */
error?: string | null;
/** 是否显示侧边栏 */
showSidebar?: boolean;
/** 右侧提问参数字段配置(可选) */
askFields?: SmartAskField[];
/** 右侧提问参数当前值(受控,可选) */
askValues?: Record<string, any>;
}
// 组件 Emits
export interface SmartSearchEmits {
/** 发送消息 */
(e: 'send', message: string): void;
/** 携带参数发送消息,不影响原 send 兼容性 */
(e: 'send-with', payload: { message: string; params: Record<string, any> }): void;
/** 新建会话 */
(e: 'new-session'): void;
/** 切换会话 */
(e: 'switch-session', sessionId: string | number): void;
/** 清空会话 */
(e: 'clear-session', sessionId: string | number): void;
/** 停止生成 */
(e: 'stop'): void;
/** 更新会话标题 */
(e: 'update-session-title', sessionId: string | number, title: string): void;
/** 更新右侧参数(受控) */
(e: 'update:askValues', values: Record<string, any>): void;
}
/** 智能搜索右侧提问参数字段类型 */
export type SmartAskFieldType =
| 'text'
| 'textarea'
| 'number'
| 'select'
| 'multi-select'
| 'radio'
| 'checkbox'
| 'switch'
| 'slider'
| 'date'
| 'date-range'
| 'file'
export interface SmartUploadConfig {
/** 上传地址 */
action: string;
/** HTTP 方法 */
method?: 'POST' | 'PUT';
/** 额外请求头 */
headers?: Record<string, string>;
/** 文件字段名,默认 'file' */
fieldName?: string;
/** 附加的表单字段 */
data?: Record<string, any>;
/** 是否多文件 */
multiple?: boolean;
/** 接受的文件类型 */
accept?: string;
/** 最大文件数 */
limit?: number;
}
export interface SmartAskField {
key: string;
label: string;
type: SmartAskFieldType;
placeholder?: string | [string, string];
options?: Array<{ label: string; value: any }>;
defaultValue?: any;
required?: boolean;
disabled?: boolean;
props?: Record<string, any>;
/** 文件上传配置(type === 'file' 生效) */
upload?: SmartUploadConfig;
}
import type { SearchConfig, FieldConfig } from '../types'
export const defaultConfig: Partial<SearchConfig> = {
display: {
fields: [
{ key: 'title', label: '标题' },
{ key: 'description', label: '描述' },
{ key: 'tags', label: '标签', type: 'tag' },
{ key: 'date', label: '日期', type: 'date' }
] as FieldConfig[],
layout: 'list',
perPage: 10,
showDetail: true
},
modes: ['simple'],
theme: {
primaryColor: '#165DFF'
}
}
export const minimalConfigExample = {
api: {
search: '/api/search'
},
display: {
fields: [
{ key: 'title', label: '标题' },
{ key: 'content', label: '内容' }
]
}
} as SearchConfig
import '@arco-design/web-vue/dist/arco.css';
import type { App } from 'vue';
import ArcoVue from '@arco-design/web-vue';
// 统一组件(组合组件)
export { default as KbSearch } from './components/KbSearch/index.vue'
// 独立组件(纯UI组件)
export { default as SimpleSearch } from './components/SimpleSearch/index.vue'
export { default as AdvancedSearch } from './components/AdvancedSearch/index.vue'
export { default as SmartSearch } from './components/SmartSearch/index.vue'
// 类型定义
export * from './types'
export * from './components/SimpleSearch/types'
export * from './components/AdvancedSearch/types'
export * from './components/SmartSearch/types'
// Hooks
export { useSearch } from './components/SimpleSearch/hooks/useSearch'
// 配置(保留兼容性)
export { defaultConfig, minimalConfigExample } from './config'
export default {
install(app: App) {
// 注册 Arco 组件库,确保 a-* 组件可用
app.use(ArcoVue);
},
};
import type { VNode } from 'vue'
export type SearchMode = 'simple' | 'advanced' | 'smart';
export interface FieldConfig {
key: string;
label: string;
type?: 'text' | 'date' | 'tag' | 'number';
searchable?: boolean;
formatter?: (value: any, item: SearchResult) => string | VNode;
render?: (value: any, item: SearchResult) => VNode;
}
export interface ApiConfig {
search: string | ((params: any) => Promise<{ data: SearchResult[]; total: number }>);
smart?: string | ((message: string) => Promise<{ content: string }>);
recommend?: string | ((params: any) => Promise<{ data: SearchResult[] }>);
}
export interface DisplayConfig {
fields: FieldConfig[];
layout?: 'list' | 'grid' | 'card';
perPage?: number;
showDetail?: boolean;
}
export interface SearchConfig {
api: ApiConfig;
display: DisplayConfig;
modes?: SearchMode[];
theme?: {
primaryColor?: string;
};
slots?: {
header?: string;
resultItem?: string;
empty?: string;
loading?: string;
};
}
export interface SearchResult {
id?: string;
title?: string;
content?: string;
description?: string;
tags?: string[];
date?: string | Date;
[key: string]: any;
}
export interface SearchParams {
keyword?: string;
filters?: Record<string, any>;
page?: number;
pageSize?: number;
mode?: SearchMode;
}
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["src/**/*", "src/**/*.vue", "types/**/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["node"],
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false,
"noEmit": false,
"jsx": "preserve",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue({
include: [
resolve(process.cwd(), 'examples/**/*.vue'),
resolve(process.cwd(), 'src/**/*.vue') // 处理 src/ 子组件
]
})
],
root: process.cwd(), // 项目根目录,服务 root index.html
resolve: {
alias: {
'@': resolve(process.cwd(), 'src') // 指向 src/
}
},
optimizeDeps: {
include: ['vue', '@arco-design/web-vue', 'axios', 'dayjs']
},
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
},
server: {
port: 5173,
open: true,
fs: {
strict: false,
allow: ['..']
},
hmr: true
},
css: {
devSourcemap: true
}
// 无 publicDir/base,使用默认
})
// 调试:日志 alias 路径
console.log('Vite demo config alias @:', resolve(process.cwd(), 'src'))
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
vue(),
dts({
entryRoot: resolve(__dirname, 'src'),
tsConfigFilePath: './tsconfig.json',
skipDiagnostics: false,
})
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'KbSearch',
fileName: 'kb-search',
formats: ['es', 'umd']
},
rollupOptions: {
external: ['vue', '@arco-design/web-vue'],
output: {
globals: {
vue: 'Vue',
'@arco-design/web-vue': 'ArcoDesign'
}
}
},
sourcemap: true,
minify: false,
cssCodeSplit: false,
target: 'es2015'
},
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论