提交 6511ef6c authored 作者: 龙菲's avatar 龙菲

fix: 更新样式

上级 24f80a37
......@@ -27,6 +27,15 @@ npm install kb-search
> **注意**:组件库不内置 axios 或 fetch,所有数据请求由业务方自行处理。
### 样式说明
**✅ 无需手动导入样式**:组件库已自动包含所有必要的样式,包括:
- Arco Design 的样式(自动导入)
- 组件库自己的样式(自动导入)
直接使用组件即可,无需额外导入任何样式文件。
---
## 快速开始
......@@ -68,30 +77,154 @@ const { keyword, results, loading, onSearch } = useSearch({
<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"
v-model:simple-keyword="keyword"
:modes="modes"
:simple="simpleConfig"
:advanced="advancedConfig"
:smart="smartConfig"
@update:smartAskValues="handleUpdateSmartAskValues"
@simple-search="handleSimpleSearch"
@advanced-search="handleAdvancedSearch"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import axios from 'axios'
import { KbSearch } from 'kb-search'
import { ref } from 'vue'
import type {
SearchMode,
SearchParams,
SearchResultItem,
PaginationInfo,
RecommendChunk,
AdvancedSearchField,
ChatSession,
ChatMessage,
SmartAskField
} from 'kb-search'
// 模式相关
const mode = ref<SearchMode>('simple')
const modes = ref<SearchMode[]>(['simple', 'advanced', 'smart'])
const mode = ref('simple')
const keyword = ref('')
const loading = ref(false)
const results = ref([])
// Simple 模式数据
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
const handleSearch = async (params) => {
// Advanced 模式数据
const fields = ref<AdvancedSearchField[]>([
{
key: 'title',
label: '标题',
type: 'text',
placeholder: '请输入标题关键词'
},
{
key: 'category',
label: '分类',
type: 'select',
options: [
{ label: '全部', value: '' },
{ label: '技术', value: 'tech' },
{ label: '商业', value: 'business' }
]
}
])
// Smart 模式数据
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const smartLoading = ref<boolean>(false)
const smartError = ref<string | null>(null)
const askFields = ref<SmartAskField[]>([])
const askValues = ref<Record<string, any>>({})
// 配置对象
const simpleConfig = computed(() => ({
keyword: keyword.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value,
recommend: recommend.value
}))
const advancedConfig = computed(() => ({
fields: fields.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const smartConfig = computed(() => ({
sessions: sessions.value,
currentSessionId: currentSessionId.value,
messages: messages.value,
loading: smartLoading.value,
error: smartError.value,
askFields: askFields.value,
askValues: askValues.value
}))
// 事件处理
const handleSimpleSearch = async (params: SearchParams) => {
loading.value = true
// 处理搜索...
loading.value = false
error.value = null
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { keyword: params.keyword, ...params }
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleUpdateSmartAskValues = (values: Record<string, any>) => {
askValues.value = values
}
</script>
```
......@@ -163,17 +296,22 @@ const handleSearch = async (params) => {
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SimpleSearch } from 'kb-search'
import { useSearch } from 'kb-search/hooks/useSearch'
import type { SearchResultItem } from 'kb-search'
const { keyword, results, loading, onSearch } = useSearch({
searchFn: async (k) => {
const res = await axios.get('/api/search', { params: { q: k } })
searchFn: async (k: string) => {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { q: k }
})
return { data: res.data.items, total: res.data.total }
}
})
const handleSearch = ({ keyword, extra }) => {
const handleSearch = ({ keyword, extra }: { keyword: string; extra?: Record<string, any> }) => {
onSearch(keyword, extra)
}
</script>
......@@ -243,32 +381,41 @@ const handleSearch = ({ keyword, extra }) => {
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SimpleSearch } from 'kb-search'
import type { SearchResultItem, RecommendChunk } from 'kb-search'
const keyword = ref('')
const loading = ref(false)
import type {
SearchResultItem,
RecommendChunk,
PaginationInfo,
SearchEventParams
} from 'kb-search'
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref({
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
const recommend = ref<RecommendChunk[]>([])
const selectedCategory = ref<string>('all')
const handleSearch = async ({ keyword: kw, extra }: { keyword: string; extra?: any }) => {
const handleSearch = async ({ keyword: kw, extra }: SearchEventParams) => {
loading.value = true
error.value = null
try {
const res = await axios.get('/api/search', { params: { q: kw, ...extra } })
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: { q: kw, ...extra }
})
results.value = res.data.items || []
pagination.value = {
current: 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: res.data.items.length < res.data.total
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
......@@ -276,6 +423,36 @@ const handleSearch = async ({ keyword: kw, extra }: { keyword: string; extra?: a
loading.value = false
}
}
const handleLoadMore = async () => {
if (!pagination.value.hasMore) return
const nextPage = (pagination.value.current || 1) + 1
loading.value = true
try {
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params: {
q: keyword.value,
page: nextPage,
pageSize: pagination.value.pageSize || 10
}
})
results.value = [...results.value, ...(res.data.items || [])]
pagination.value = {
...pagination.value,
current: nextPage,
hasMore: results.value.length < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '加载失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑,如跳转到详情页
}
</script>
```
......@@ -314,15 +491,24 @@ const handleSearch = async ({ keyword: kw, extra }: { keyword: string; extra?: a
:fields="fields"
:loading="loading"
:error="error"
:results="results"
:pagination="pagination"
@search="handleAdvancedSearch"
@load-more="handleLoadMore"
@select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { AdvancedSearch } from 'kb-search'
import type { AdvancedSearchField, SearchParams } from 'kb-search'
import type { SearchResultItem, PaginationInfo } from 'kb-search'
import type {
AdvancedSearchField,
SearchParams,
SearchResultItem,
PaginationInfo
} from 'kb-search'
// 定义表单字段配置
const fields = ref<AdvancedSearchField[]>([
......@@ -372,7 +558,7 @@ const fields = ref<AdvancedSearchField[]>([
}
])
const loading = ref(false)
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
......@@ -388,7 +574,7 @@ const handleAdvancedSearch = async (params: SearchParams) => {
error.value = null
try {
// 调用后端 API,filters 包含所有表单字段的值
const res = await axios.post('/api/advanced-search', {
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
......@@ -400,7 +586,7 @@ const handleAdvancedSearch = async (params: SearchParams) => {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: res.data.items.length < res.data.total
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
......@@ -408,6 +594,34 @@ const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = false
}
}
const handleLoadMore = async () => {
if (!pagination.value.hasMore) return
const nextPage = (pagination.value.current || 1) + 1
loading.value = true
try {
// 获取当前搜索条件(需要根据实际情况保存)
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
page: nextPage,
pageSize: pagination.value.pageSize || 10
})
results.value = [...results.value, ...(res.data.items || [])]
pagination.value = {
...pagination.value,
current: nextPage,
hasMore: results.value.length < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '加载失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑
}
</script>
```
......@@ -479,34 +693,46 @@ const handleAdvancedSearch = async (params: SearchParams) => {
:messages="messages"
:loading="loading"
:error="error"
:ask-fields="askFields"
:ask-values="askValues"
@send="handleSend"
@new-session="handleNewSession"
@switch-session="handleSwitchSession"
@clear-session="handleClearSession"
@stop="handleStop"
@update:askValues="handleUpdateAskValues"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { SmartSearch } from 'kb-search'
import type { ChatSession, ChatMessage } from 'kb-search'
import type {
ChatSession,
ChatMessage,
SmartAskField
} from 'kb-search'
const sessions = ref<ChatSession[]>([])
const currentSessionId = ref<string | number>('')
const messages = ref<ChatMessage[]>([])
const loading = ref(false)
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const askFields = ref<SmartAskField[]>([])
const askValues = ref<Record<string, any>>({})
const handleSend = async (message: string) => {
loading.value = true
error.value = null
try {
// 添加用户消息
messages.value.push({
const userMsg: ChatMessage = {
id: Date.now(),
role: 'user',
content: message
})
}
messages.value.push(userMsg)
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
......@@ -517,14 +743,60 @@ const handleSend = async (message: string) => {
messages.value.push(aiMsg)
// 调用 API(支持流式响应)
const res = await axios.post('/api/ai/chat', { message })
const res = await axios.post<{ content: string }>('/api/ai/chat', {
message,
params: askValues.value
})
aiMsg.content = res.data.content
} catch (err: any) {
error.value = err.message || '发送失败'
// 移除失败的 AI 消息占位
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
} finally {
loading.value = false
}
}
const handleNewSession = () => {
const newSession: ChatSession = {
id: Date.now(),
title: '新会话',
createdAt: new Date()
}
sessions.value.push(newSession)
currentSessionId.value = newSession.id
messages.value = []
}
const handleSwitchSession = (sessionId: string | number) => {
currentSessionId.value = sessionId
// 根据 sessionId 加载对应的消息(需要根据实际情况实现)
// messages.value = loadMessagesBySessionId(sessionId)
}
const handleClearSession = (sessionId: string | number) => {
messages.value = []
// 可选:从 sessions 中移除该会话
const index = sessions.value.findIndex(s => s.id === sessionId)
if (index > -1) {
sessions.value.splice(index, 1)
}
if (currentSessionId.value === sessionId) {
currentSessionId.value = ''
}
}
const handleStop = () => {
loading.value = false
// 停止流式响应(需要根据实际情况实现)
}
const handleUpdateAskValues = (values: Record<string, any>) => {
askValues.value = values
}
</script>
```
......@@ -566,43 +838,112 @@ const handleSend = async (message: string) => {
<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"
v-model:simple-keyword="keyword"
:modes="modes"
:simple="simpleConfig"
:advanced="advancedConfig"
@simple-search="handleSimpleSearch"
@advanced-search="handleAdvancedSearch"
@simple-select="handleSelect"
@advanced-select="handleSelect"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import axios from 'axios'
import { KbSearch } from 'kb-search'
import type { SearchMode, SearchParams } from 'kb-search'
import type {
SearchMode,
SearchParams,
SearchResultItem,
PaginationInfo,
AdvancedSearchField
} 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 modes = ref<SearchMode[]>(['simple', 'advanced'])
const handleSearch = async (params: SearchParams) => {
// Simple 模式数据
const keyword = ref<string>('')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const results = ref<SearchResultItem[]>([])
const pagination = ref<PaginationInfo>({
current: 1,
pageSize: 10,
total: 0,
hasMore: false
})
// Advanced 模式数据(示例)
const fields = ref<AdvancedSearchField[]>([])
// 配置对象
const simpleConfig = computed(() => ({
keyword: keyword.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const advancedConfig = computed(() => ({
fields: fields.value,
loading: loading.value,
error: error.value,
results: results.value,
pagination: pagination.value
}))
const handleSimpleSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.get('/api/search', { params })
const res = await axios.get<{ items: SearchResultItem[]; total: number }>('/api/search', {
params
})
results.value = res.data.items || []
pagination.value = {
current: 1,
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleAdvancedSearch = async (params: SearchParams) => {
loading.value = true
error.value = null
try {
const res = await axios.post<{ items: SearchResultItem[]; total: number }>('/api/advanced-search', {
filters: params.filters,
page: params.page || 1,
pageSize: 10
})
results.value = res.data.items || []
pagination.value = {
current: params.page || 1,
pageSize: 10,
total: res.data.total || 0,
hasMore: res.data.items.length < res.data.total
hasMore: (res.data.items?.length || 0) < (res.data.total || 0)
}
} catch (err: any) {
error.value = err.message || '搜索失败'
} finally {
loading.value = false
}
}
const handleSelect = (item: SearchResultItem) => {
console.log('选中结果:', item)
// 处理选中逻辑
}
</script>
```
......@@ -774,9 +1115,24 @@ const handleSearch = async (params) => {
### 流式响应处理(智能搜索)
```ts
import { ref } from 'vue'
import type { ChatMessage } from 'kb-search'
const messages = ref<ChatMessage[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const handleSend = async (message: string) => {
loading.value = true
error.value = null
// 添加用户消息
messages.value.push({ id: Date.now(), role: 'user', content: message })
const userMsg: ChatMessage = {
id: Date.now(),
role: 'user',
content: message
}
messages.value.push(userMsg)
// 添加 AI 消息占位
const aiMsg: ChatMessage = {
......@@ -786,20 +1142,43 @@ const handleSend = async (message: string) => {
}
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
try {
// 流式响应
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error('无法获取响应流')
}
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
aiMsg.content += chunk
}
} catch (err: any) {
error.value = err.message || '发送失败'
// 移除失败的 AI 消息占位
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
} finally {
loading.value = false
}
}
```
......
{"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;AAI/B,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAGrE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAC7E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAA;AAG3E,cAAc,SAAS,CAAA;AACvB,cAAc,iCAAiC,CAAA;AAC/C,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAG9C,OAAO,EAAE,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAGrE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;;iBAK/C,GAAG;;AADlB,wBAKE"}
\ No newline at end of file
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAI/B,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAGrE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAC7E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAA;AAI3E,OAAO,iCAAiC,CAAA;AACxC,OAAO,qCAAqC,CAAA;AAC5C,OAAO,uCAAuC,CAAA;AAC9C,OAAO,oCAAoC,CAAA;AAG3C,cAAc,SAAS,CAAA;AACvB,cAAc,iCAAiC,CAAA;AAC/C,cAAc,mCAAmC,CAAA;AACjD,cAAc,gCAAgC,CAAA;AAG9C,OAAO,EAAE,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAGrE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;;iBAK/C,GAAG;;AADlB,wBAKE"}
\ No newline at end of file
......@@ -38,7 +38,7 @@ var __async = (__this, __arguments, generator) => {
});
};
import ArcoVue from "@arco-design/web-vue";
import { getCurrentInstance, inject, defineComponent, createElementBlock, openBlock, normalizeStyle, normalizeClass, createElementVNode, computed, ref, watch, resolveComponent, createVNode, createCommentVNode, toDisplayString, Fragment, renderList, renderSlot, pushScopeId, popScopeId, withCtx, createTextVNode, unref, createBlock, mergeProps, onMounted, createStaticVNode, withKeys, withModifiers, nextTick } from "vue";
import { getCurrentInstance, inject, defineComponent, computed, createElementBlock, openBlock, normalizeStyle, normalizeClass, createElementVNode, ref, watch, resolveComponent, createVNode, createCommentVNode, toDisplayString, Fragment, renderList, renderSlot, pushScopeId, popScopeId, withCtx, createTextVNode, unref, createBlock, mergeProps, onMounted, createStaticVNode, withKeys, withModifiers, nextTick } from "vue";
const configProviderInjectionKey = Symbol("ArcoConfigProvider");
const CLASS_PREFIX = "arco";
const GLOBAL_CONFIG_NAME = "$arco";
......
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.
......@@ -11,7 +11,9 @@
"types": "./dist/index.d.ts",
"import": "./dist/kb-search.js",
"require": "./dist/kb-search.umd.cjs"
}
},
"./style.css": "./dist/style.css",
"./dist/style.css": "./dist/style.css"
},
"files": [
"dist",
......
// 自动导入 Arco Design 样式(用户无需手动引入)
import '@arco-design/web-vue/dist/arco.css';
// 自动导入组件库自己的样式(用户无需手动引入)
import './components/SimpleSearch/style.css';
import type { App } from 'vue';
import ArcoVue from '@arco-design/web-vue';
......@@ -10,6 +15,13 @@ 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'
// 导入所有组件以确保它们的样式被提取
// 这些导入不会影响导出,但会让 Vite 处理组件的所有依赖(包括样式)
import './components/KbSearch/index.vue'
import './components/SimpleSearch/index.vue'
import './components/AdvancedSearch/index.vue'
import './components/SmartSearch/index.vue'
// 类型定义
export * from './types'
export * from './components/SimpleSearch/types'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论