fix: Update private key field in chat integration documentation
- Change the private key field name from "prvkey" to "prvkey_hex" in the CHAT_INTEGRATION.md file for clarity and consistency with expected data formats. feat: Add Fuzzy Search Component and Composable - Introduce a new FuzzySearch component for Vue 3, leveraging Fuse.js for intelligent search capabilities. - Implement a useFuzzySearch composable for flexible search functionality, allowing configuration of search options. - Create demo and README files to showcase usage and features of the fuzzy search implementation. - Update package.json and package-lock.json to include @vueuse/integrations version 13.6.0 for enhanced performance.
This commit is contained in:
parent
37a539bc2d
commit
3d1bc94183
8 changed files with 783 additions and 1 deletions
|
|
@ -59,7 +59,7 @@ Response:
|
||||||
"username": "username",
|
"username": "username",
|
||||||
"email": "email@example.com",
|
"email": "email@example.com",
|
||||||
"pubkey": "nostr_public_key",
|
"pubkey": "nostr_public_key",
|
||||||
"prvkey": "nostr_private_key",
|
"prvkey": "nostr_private_key_hex",
|
||||||
"created_at": "2024-01-01T00:00:00Z",
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
"updated_at": "2024-01-01T00:00:00Z"
|
"updated_at": "2024-01-01T00:00:00Z"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
package-lock.json
generated
111
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"@vueuse/components": "^12.5.0",
|
"@vueuse/components": "^12.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@vueuse/head": "^2.0.0",
|
"@vueuse/head": "^2.0.0",
|
||||||
|
"@vueuse/integrations": "^13.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
@ -5508,6 +5509,116 @@
|
||||||
"vue": ">=2.7 || >=3"
|
"vue": ">=2.7 || >=3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/integrations": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-dVFdgwYvkYjdizRL3ESdUW+Hg84i9Yhuzs+Ec3kEcuzJmT5xhiL/IGdw4z394qSBngUQvFi+wbHwhHX3EGbAxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "13.6.0",
|
||||||
|
"@vueuse/shared": "13.6.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"async-validator": "^4",
|
||||||
|
"axios": "^1",
|
||||||
|
"change-case": "^5",
|
||||||
|
"drauu": "^0.4",
|
||||||
|
"focus-trap": "^7",
|
||||||
|
"fuse.js": "^7",
|
||||||
|
"idb-keyval": "^6",
|
||||||
|
"jwt-decode": "^4",
|
||||||
|
"nprogress": "^0.2",
|
||||||
|
"qrcode": "^1.5",
|
||||||
|
"sortablejs": "^1",
|
||||||
|
"universal-cookie": "^7 || ^8",
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"async-validator": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"axios": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"change-case": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"drauu": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"focus-trap": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"fuse.js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"idb-keyval": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jwt-decode": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nprogress": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"qrcode": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sortablejs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"universal-cookie": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
"@vueuse/metadata": "13.6.0",
|
||||||
|
"@vueuse/shared": "13.6.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vueuse/metadata": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "12.8.2",
|
"version": "12.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"@vueuse/components": "^12.5.0",
|
"@vueuse/components": "^12.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@vueuse/head": "^2.0.0",
|
"@vueuse/head": "^2.0.0",
|
||||||
|
"@vueuse/integrations": "^13.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|
|
||||||
133
src/components/ui/fuzzy-search/FuzzySearch.vue
Normal file
133
src/components/ui/fuzzy-search/FuzzySearch.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { useFuzzySearch, type FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Search, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props<T = any> {
|
||||||
|
/**
|
||||||
|
* The data to search through
|
||||||
|
*/
|
||||||
|
data: T[]
|
||||||
|
/**
|
||||||
|
* Configuration options for the fuzzy search
|
||||||
|
*/
|
||||||
|
options?: FuzzySearchOptions<T>
|
||||||
|
/**
|
||||||
|
* Placeholder text for the search input
|
||||||
|
*/
|
||||||
|
placeholder?: string
|
||||||
|
/**
|
||||||
|
* Whether to show a clear button
|
||||||
|
*/
|
||||||
|
showClearButton?: boolean
|
||||||
|
/**
|
||||||
|
* Whether to show the result count
|
||||||
|
*/
|
||||||
|
showResultCount?: boolean
|
||||||
|
/**
|
||||||
|
* Custom class for the search container
|
||||||
|
*/
|
||||||
|
class?: string
|
||||||
|
/**
|
||||||
|
* Whether the search input should be disabled
|
||||||
|
*/
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits<T = any> {
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
(e: 'search', query: string): void
|
||||||
|
(e: 'results', results: T[]): void
|
||||||
|
(e: 'clear'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
placeholder: 'Search...',
|
||||||
|
showClearButton: true,
|
||||||
|
showResultCount: true,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// Create reactive data ref for the composable
|
||||||
|
const dataRef = computed(() => props.data)
|
||||||
|
|
||||||
|
// Use the fuzzy search composable
|
||||||
|
const {
|
||||||
|
searchQuery,
|
||||||
|
results,
|
||||||
|
filteredItems,
|
||||||
|
isSearching,
|
||||||
|
resultCount,
|
||||||
|
clearSearch,
|
||||||
|
setSearchQuery
|
||||||
|
} = useFuzzySearch(dataRef, props.options)
|
||||||
|
|
||||||
|
// Emit events when search changes
|
||||||
|
const handleSearchChange = (value: string | number) => {
|
||||||
|
const stringValue = String(value)
|
||||||
|
setSearchQuery(stringValue)
|
||||||
|
emit('update:modelValue', stringValue)
|
||||||
|
emit('search', stringValue)
|
||||||
|
emit('results', filteredItems.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
clearSearch()
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
emit('search', '')
|
||||||
|
emit('results', filteredItems.value)
|
||||||
|
emit('clear')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for changes in filtered items and emit results
|
||||||
|
watch(filteredItems, (items) => {
|
||||||
|
emit('results', items)
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="['fuzzy-search', class]">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Search class="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
:model-value="searchQuery"
|
||||||
|
@update:model-value="handleSearchChange"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="pl-10 pr-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Clear Button -->
|
||||||
|
<div v-if="showClearButton && searchQuery" class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="handleClear"
|
||||||
|
class="h-6 w-6 p-0 hover:bg-muted"
|
||||||
|
>
|
||||||
|
<X class="h-3 w-3" />
|
||||||
|
<span class="sr-only">Clear search</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result Count -->
|
||||||
|
<div v-if="showResultCount && isSearching" class="mt-2 text-sm text-muted-foreground">
|
||||||
|
{{ resultCount }} result{{ resultCount === 1 ? '' : 's' }} found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fuzzy-search {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/components/ui/fuzzy-search/FuzzySearchDemo.vue
Normal file
116
src/components/ui/fuzzy-search/FuzzySearchDemo.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { FuzzySearch, useFuzzySearch } from './index'
|
||||||
|
|
||||||
|
// Sample data for demonstration
|
||||||
|
interface Product {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
price: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const products = ref<Product[]>([
|
||||||
|
{ id: 1, name: 'Laptop', description: 'High-performance laptop for work and gaming', category: 'Electronics', price: 1200 },
|
||||||
|
{ id: 2, name: 'Smartphone', description: 'Latest smartphone with advanced features', category: 'Electronics', price: 800 },
|
||||||
|
{ id: 3, name: 'Headphones', description: 'Wireless noise-cancelling headphones', category: 'Audio', price: 200 },
|
||||||
|
{ id: 4, name: 'Coffee Maker', description: 'Automatic coffee maker for home use', category: 'Kitchen', price: 150 },
|
||||||
|
{ id: 5, name: 'Running Shoes', description: 'Comfortable running shoes for athletes', category: 'Sports', price: 120 },
|
||||||
|
{ id: 6, name: 'Backpack', description: 'Durable backpack for travel and daily use', category: 'Travel', price: 80 },
|
||||||
|
{ id: 7, name: 'Tablet', description: 'Portable tablet for entertainment and work', category: 'Electronics', price: 500 },
|
||||||
|
{ id: 8, name: 'Blender', description: 'High-speed blender for smoothies and shakes', category: 'Kitchen', price: 100 },
|
||||||
|
])
|
||||||
|
|
||||||
|
// Fuzzy search configuration
|
||||||
|
const searchOptions = {
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ['name', 'description', 'category'],
|
||||||
|
threshold: 0.3,
|
||||||
|
distance: 100,
|
||||||
|
ignoreLocation: true,
|
||||||
|
useExtendedSearch: false,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
shouldSort: true,
|
||||||
|
findAllMatches: false,
|
||||||
|
location: 0,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
},
|
||||||
|
resultLimit: 10,
|
||||||
|
matchAllWhenSearchEmpty: true,
|
||||||
|
minSearchLength: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the fuzzy search composable
|
||||||
|
const {
|
||||||
|
searchQuery,
|
||||||
|
results,
|
||||||
|
filteredItems,
|
||||||
|
isSearching,
|
||||||
|
resultCount,
|
||||||
|
clearSearch
|
||||||
|
} = useFuzzySearch(products, searchOptions)
|
||||||
|
|
||||||
|
// Handle search results
|
||||||
|
const handleSearchResults = (results: Product[]) => {
|
||||||
|
console.log('Search results:', results)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
console.log('Search query:', query)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6 max-w-4xl mx-auto">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">Fuzzy Search Demo</h1>
|
||||||
|
|
||||||
|
<!-- Fuzzy Search Component -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Search Products</h2>
|
||||||
|
<FuzzySearch
|
||||||
|
:data="products"
|
||||||
|
:options="searchOptions"
|
||||||
|
placeholder="Search products by name, description, or category..."
|
||||||
|
@search="handleSearch"
|
||||||
|
@results="handleSearchResults"
|
||||||
|
class="max-w-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
Results ({{ resultCount }} found)
|
||||||
|
<span v-if="isSearching" class="text-sm font-normal text-muted-foreground">
|
||||||
|
- Searching for "{{ searchQuery }}"
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-if="filteredItems.length === 0 && isSearching" class="text-center py-8 text-muted-foreground">
|
||||||
|
No products found matching your search.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="product in filteredItems"
|
||||||
|
:key="product.id"
|
||||||
|
class="border rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<h3 class="font-semibold text-lg">{{ product.name }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground mb-2">{{ product.description }}</p>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm bg-secondary px-2 py-1 rounded">{{ product.category }}</span>
|
||||||
|
<span class="font-semibold">${{ product.price }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Raw Results (for debugging) -->
|
||||||
|
<div v-if="isSearching" class="mt-8 p-4 bg-muted rounded-lg">
|
||||||
|
<h3 class="font-semibold mb-2">Raw Search Results (with scoring)</h3>
|
||||||
|
<pre class="text-xs overflow-auto">{{ JSON.stringify(results, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
236
src/components/ui/fuzzy-search/README.md
Normal file
236
src/components/ui/fuzzy-search/README.md
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
# Fuzzy Search Component
|
||||||
|
|
||||||
|
A powerful fuzzy search implementation for Vue 3 using Fuse.js and VueUse integrations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔍 **Fuzzy Search**: Intelligent search with typo tolerance and scoring
|
||||||
|
- ⚡ **VueUse Integration**: Built on top of `@vueuse/integrations` for optimal performance
|
||||||
|
- 🎯 **Configurable**: Customizable search options and behavior
|
||||||
|
- 🎨 **Vue 3 Compatible**: Built with Vue 3 Composition API and TypeScript
|
||||||
|
- 📦 **Reusable**: Both composable and component versions available
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The fuzzy search functionality is already installed and available in this project. It uses:
|
||||||
|
|
||||||
|
- `fuse.js` - For fuzzy search algorithms
|
||||||
|
- `@vueuse/integrations` - For Vue 3 integration
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Using the Composable
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const products = ref<Product[]>([
|
||||||
|
{ id: 1, name: 'Laptop', description: 'High-performance laptop', category: 'Electronics' },
|
||||||
|
{ id: 2, name: 'Smartphone', description: 'Latest smartphone', category: 'Electronics' },
|
||||||
|
// ... more products
|
||||||
|
])
|
||||||
|
|
||||||
|
const { searchQuery, filteredItems, isSearching, resultCount, clearSearch } = useFuzzySearch(
|
||||||
|
products,
|
||||||
|
{
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ['name', 'description', 'category'],
|
||||||
|
threshold: 0.3,
|
||||||
|
},
|
||||||
|
resultLimit: 10,
|
||||||
|
minSearchLength: 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input v-model="searchQuery" placeholder="Search products..." />
|
||||||
|
<p>Found {{ resultCount }} results</p>
|
||||||
|
|
||||||
|
<div v-for="product in filteredItems" :key="product.id">
|
||||||
|
{{ product.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the Component
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { FuzzySearch } from '@/components/ui/fuzzy-search'
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const products = ref<Product[]>([
|
||||||
|
{ id: 1, name: 'Laptop', description: 'High-performance laptop' },
|
||||||
|
{ id: 2, name: 'Smartphone', description: 'Latest smartphone' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleSearchResults = (results: Product[]) => {
|
||||||
|
console.log('Search results:', results)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FuzzySearch
|
||||||
|
:data="products"
|
||||||
|
:options="{
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
},
|
||||||
|
resultLimit: 10,
|
||||||
|
}"
|
||||||
|
placeholder="Search products..."
|
||||||
|
@results="handleSearchResults"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `useFuzzySearch` Composable
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `data: Ref<T[]> | T[]` - The data to search through
|
||||||
|
- `options?: FuzzySearchOptions<T>` - Configuration options
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
- `searchQuery: Ref<string>` - Current search query
|
||||||
|
- `results: ComputedRef<FuseResult<T>[]>` - Search results with scoring
|
||||||
|
- `filteredItems: ComputedRef<T[]>` - Filtered items (just the items)
|
||||||
|
- `isSearching: ComputedRef<boolean>` - Whether search is active
|
||||||
|
- `resultCount: ComputedRef<number>` - Number of results found
|
||||||
|
- `clearSearch: () => void` - Clear the current search
|
||||||
|
- `setSearchQuery: (query: string) => void` - Set the search query
|
||||||
|
- `updateData: (data: T[]) => void` - Update the data to search through
|
||||||
|
|
||||||
|
### `FuzzySearch` Component
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
- `data: T[]` - The data to search through
|
||||||
|
- `options?: FuzzySearchOptions<T>` - Configuration options
|
||||||
|
- `placeholder?: string` - Placeholder text for the search input
|
||||||
|
- `showClearButton?: boolean` - Whether to show a clear button
|
||||||
|
- `showResultCount?: boolean` - Whether to show the result count
|
||||||
|
- `class?: string` - Custom class for the search container
|
||||||
|
- `disabled?: boolean` - Whether the search input should be disabled
|
||||||
|
|
||||||
|
#### Events
|
||||||
|
|
||||||
|
- `update:modelValue` - Emitted when the search query changes
|
||||||
|
- `search` - Emitted when a search is performed
|
||||||
|
- `results` - Emitted when search results change
|
||||||
|
- `clear` - Emitted when the search is cleared
|
||||||
|
|
||||||
|
### `FuzzySearchOptions`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FuzzySearchOptions<T = any> {
|
||||||
|
/**
|
||||||
|
* Fuse.js options for configuring the search behavior
|
||||||
|
*/
|
||||||
|
fuseOptions?: FuseOptions<T>
|
||||||
|
/**
|
||||||
|
* Maximum number of results to return
|
||||||
|
*/
|
||||||
|
resultLimit?: number
|
||||||
|
/**
|
||||||
|
* Whether to return all items when search is empty
|
||||||
|
*/
|
||||||
|
matchAllWhenSearchEmpty?: boolean
|
||||||
|
/**
|
||||||
|
* Debounce delay in milliseconds for search input
|
||||||
|
*/
|
||||||
|
debounceMs?: number
|
||||||
|
/**
|
||||||
|
* Minimum search length before triggering search
|
||||||
|
*/
|
||||||
|
minSearchLength?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Basic Search
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const options = {
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Search
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const options = {
|
||||||
|
fuseOptions: {
|
||||||
|
keys: [
|
||||||
|
{ name: 'name', weight: 0.7 },
|
||||||
|
{ name: 'description', weight: 0.3 },
|
||||||
|
{ name: 'category', weight: 0.2 },
|
||||||
|
],
|
||||||
|
threshold: 0.2,
|
||||||
|
distance: 100,
|
||||||
|
ignoreLocation: true,
|
||||||
|
useExtendedSearch: false,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
shouldSort: true,
|
||||||
|
findAllMatches: false,
|
||||||
|
location: 0,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
},
|
||||||
|
resultLimit: 20,
|
||||||
|
matchAllWhenSearchEmpty: true,
|
||||||
|
minSearchLength: 2,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search with Weighted Keys
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const options = {
|
||||||
|
fuseOptions: {
|
||||||
|
keys: [
|
||||||
|
{ name: 'title', weight: 0.8 },
|
||||||
|
{ name: 'content', weight: 0.5 },
|
||||||
|
{ name: 'tags', weight: 0.3 },
|
||||||
|
],
|
||||||
|
threshold: 0.4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Use `resultLimit`** to limit the number of results for better performance
|
||||||
|
2. **Set appropriate `threshold`** values (0.0 = perfect match, 1.0 = match anything)
|
||||||
|
3. **Use weighted keys** to prioritize certain fields
|
||||||
|
4. **Consider `minSearchLength`** to avoid searching on very short queries
|
||||||
|
5. **Use `ignoreLocation: true`** for better performance when location doesn't matter
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See `FuzzySearchDemo.vue` for a complete example of how to use the fuzzy search functionality.
|
||||||
2
src/components/ui/fuzzy-search/index.ts
Normal file
2
src/components/ui/fuzzy-search/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as FuzzySearch } from './FuzzySearch.vue'
|
||||||
|
export { useFuzzySearch, type FuzzySearchOptions, type UseFuzzySearchReturn } from '@/composables/useFuzzySearch'
|
||||||
183
src/composables/useFuzzySearch.ts
Normal file
183
src/composables/useFuzzySearch.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
||||||
|
import { useFuse, type UseFuseOptions, type FuseOptions } from '@vueuse/integrations'
|
||||||
|
import type { FuseResult } from 'fuse.js'
|
||||||
|
|
||||||
|
export interface FuzzySearchOptions<T = any> {
|
||||||
|
/**
|
||||||
|
* Fuse.js options for configuring the search behavior
|
||||||
|
*/
|
||||||
|
fuseOptions?: FuseOptions<T>
|
||||||
|
/**
|
||||||
|
* Maximum number of results to return
|
||||||
|
*/
|
||||||
|
resultLimit?: number
|
||||||
|
/**
|
||||||
|
* Whether to return all items when search is empty
|
||||||
|
*/
|
||||||
|
matchAllWhenSearchEmpty?: boolean
|
||||||
|
/**
|
||||||
|
* Debounce delay in milliseconds for search input
|
||||||
|
*/
|
||||||
|
debounceMs?: number
|
||||||
|
/**
|
||||||
|
* Minimum search length before triggering search
|
||||||
|
*/
|
||||||
|
minSearchLength?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseFuzzySearchReturn<T> {
|
||||||
|
/**
|
||||||
|
* Current search query
|
||||||
|
*/
|
||||||
|
searchQuery: Ref<string>
|
||||||
|
/**
|
||||||
|
* Search results with Fuse.js scoring
|
||||||
|
*/
|
||||||
|
results: ComputedRef<FuseResult<T>[]>
|
||||||
|
/**
|
||||||
|
* Filtered items (just the items without scoring)
|
||||||
|
*/
|
||||||
|
filteredItems: ComputedRef<T[]>
|
||||||
|
/**
|
||||||
|
* Whether search is currently active
|
||||||
|
*/
|
||||||
|
isSearching: ComputedRef<boolean>
|
||||||
|
/**
|
||||||
|
* Number of results found
|
||||||
|
*/
|
||||||
|
resultCount: ComputedRef<number>
|
||||||
|
/**
|
||||||
|
* Clear the current search
|
||||||
|
*/
|
||||||
|
clearSearch: () => void
|
||||||
|
/**
|
||||||
|
* Set the search query
|
||||||
|
*/
|
||||||
|
setSearchQuery: (query: string) => void
|
||||||
|
/**
|
||||||
|
* Update the data to search through
|
||||||
|
*/
|
||||||
|
updateData: (data: T[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for fuzzy search functionality using Fuse.js
|
||||||
|
*
|
||||||
|
* @param data - The data to search through
|
||||||
|
* @param options - Configuration options for the fuzzy search
|
||||||
|
* @returns Object with search functionality and results
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Basic usage
|
||||||
|
* const { searchQuery, results, filteredItems, clearSearch } = useFuzzySearch(
|
||||||
|
* products,
|
||||||
|
* {
|
||||||
|
* fuseOptions: {
|
||||||
|
* keys: ['name', 'description'],
|
||||||
|
* threshold: 0.3
|
||||||
|
* },
|
||||||
|
* resultLimit: 10,
|
||||||
|
* minSearchLength: 2
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // Integration with existing market functionality
|
||||||
|
* const { searchQuery, filteredItems } = useFuzzySearch(
|
||||||
|
* marketStore.products,
|
||||||
|
* {
|
||||||
|
* fuseOptions: {
|
||||||
|
* keys: [
|
||||||
|
* { name: 'name', weight: 0.7 },
|
||||||
|
* { name: 'description', weight: 0.3 },
|
||||||
|
* { name: 'stallName', weight: 0.2 },
|
||||||
|
* { name: 'categories', weight: 0.1 }
|
||||||
|
* ],
|
||||||
|
* threshold: 0.3,
|
||||||
|
* ignoreLocation: true
|
||||||
|
* },
|
||||||
|
* resultLimit: 50,
|
||||||
|
* minSearchLength: 2
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useFuzzySearch<T = any>(
|
||||||
|
data: Ref<T[]> | T[],
|
||||||
|
options: FuzzySearchOptions<T> = {}
|
||||||
|
): UseFuzzySearchReturn<T> {
|
||||||
|
const {
|
||||||
|
fuseOptions = {
|
||||||
|
// Default Fuse.js options
|
||||||
|
threshold: 0.3,
|
||||||
|
distance: 100,
|
||||||
|
ignoreLocation: true,
|
||||||
|
useExtendedSearch: false,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
shouldSort: true,
|
||||||
|
findAllMatches: false,
|
||||||
|
location: 0,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
keys: []
|
||||||
|
},
|
||||||
|
resultLimit,
|
||||||
|
matchAllWhenSearchEmpty = true,
|
||||||
|
debounceMs = 300,
|
||||||
|
minSearchLength = 0
|
||||||
|
} = options
|
||||||
|
|
||||||
|
// Search query state
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
// Create the Fuse instance using VueUse integration
|
||||||
|
const { results } = useFuse(
|
||||||
|
searchQuery,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
fuseOptions,
|
||||||
|
resultLimit,
|
||||||
|
matchAllWhenSearchEmpty
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const isSearching = computed(() => {
|
||||||
|
const query = searchQuery.value.trim()
|
||||||
|
return query.length >= minSearchLength
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
return results.value.map(result => result.item)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultCount = computed(() => results.value.length)
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSearchQuery = (query: string) => {
|
||||||
|
searchQuery.value = query
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = (newData: T[]) => {
|
||||||
|
// The useFuse composable automatically watches for data changes
|
||||||
|
// so we just need to update the ref if it's reactive
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
// If data is not reactive, we need to handle this differently
|
||||||
|
console.warn('Data is not reactive. Consider using ref() for the data parameter.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
results,
|
||||||
|
filteredItems,
|
||||||
|
isSearching,
|
||||||
|
resultCount,
|
||||||
|
clearSearch,
|
||||||
|
setSearchQuery,
|
||||||
|
updateData
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue