244 lines
6.0 KiB
Vue
244 lines
6.0 KiB
Vue
|
|
<!--
|
|||
|
|
=============================================================================
|
|||
|
|
企微IT智能服务台 — 全局搜索组件
|
|||
|
|
=============================================================================
|
|||
|
|
说明:全局搜索组件,支持搜索配置项、坐席、快速回复
|
|||
|
|
回车或点搜索图标触发搜索,结果以下拉面板展示
|
|||
|
|
-->
|
|||
|
|
<template>
|
|||
|
|
<div class="search-box-wrapper">
|
|||
|
|
<el-popover
|
|||
|
|
:visible="showResults"
|
|||
|
|
placement="bottom-end"
|
|||
|
|
:width="320"
|
|||
|
|
trigger="manual"
|
|||
|
|
:popper-style="{ background: 'var(--bg-secondary)', border: '1px solid var(--border)' }"
|
|||
|
|
:show-arrow="false"
|
|||
|
|
>
|
|||
|
|
<template #reference>
|
|||
|
|
<div class="search-box" @click="showResults = searchText.length > 0">
|
|||
|
|
<el-icon :size="14" style="color: var(--text-muted)"><Search /></el-icon>
|
|||
|
|
<input
|
|||
|
|
v-model="searchText"
|
|||
|
|
type="text"
|
|||
|
|
placeholder="搜索功能或配置..."
|
|||
|
|
class="search-input"
|
|||
|
|
@keydown.enter="handleSearch"
|
|||
|
|
@input="handleInput"
|
|||
|
|
/>
|
|||
|
|
<el-icon
|
|||
|
|
v-if="searchText"
|
|||
|
|
:size="14"
|
|||
|
|
style="color: var(--text-muted); cursor: pointer"
|
|||
|
|
@click="clearSearch"
|
|||
|
|
>
|
|||
|
|
<Close />
|
|||
|
|
</el-icon>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<!-- 搜索结果 -->
|
|||
|
|
<div class="search-results">
|
|||
|
|
<div v-if="loading" class="search-loading">搜索中...</div>
|
|||
|
|
<template v-else-if="results.length > 0">
|
|||
|
|
<div class="search-result-count">找到 {{ results.length }} 条结果</div>
|
|||
|
|
<div
|
|||
|
|
v-for="item in results"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="search-result-item"
|
|||
|
|
@click="navigateTo(item.route)"
|
|||
|
|
>
|
|||
|
|
<el-tag :type="getResultTagType(item.type)" size="small" effect="plain">
|
|||
|
|
{{ getResultTypeText(item.type) }}
|
|||
|
|
</el-tag>
|
|||
|
|
<span class="result-name">{{ item.name }}</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<div v-else-if="searched" class="search-empty">暂无匹配结果</div>
|
|||
|
|
</div>
|
|||
|
|
</el-popover>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
// ==========================================================================
|
|||
|
|
// 依赖导入
|
|||
|
|
// ==========================================================================
|
|||
|
|
import { ref } from 'vue'
|
|||
|
|
import { useRouter } from 'vue-router'
|
|||
|
|
import type { SearchResultItem } from '@/types'
|
|||
|
|
import { globalSearch } from '@/api/admin'
|
|||
|
|
|
|||
|
|
// ==========================================================================
|
|||
|
|
// 路由
|
|||
|
|
// ==========================================================================
|
|||
|
|
const router = useRouter()
|
|||
|
|
|
|||
|
|
// ==========================================================================
|
|||
|
|
// 状态
|
|||
|
|
// ==========================================================================
|
|||
|
|
|
|||
|
|
/** 搜索文本 */
|
|||
|
|
const searchText = ref<string>('')
|
|||
|
|
|
|||
|
|
/** 是否显示搜索结果 */
|
|||
|
|
const showResults = ref<boolean>(false)
|
|||
|
|
|
|||
|
|
/** 搜索结果 */
|
|||
|
|
const results = ref<SearchResultItem[]>([])
|
|||
|
|
|
|||
|
|
/** 是否已搜索过 */
|
|||
|
|
const searched = ref<boolean>(false)
|
|||
|
|
|
|||
|
|
/** 是否正在加载 */
|
|||
|
|
const loading = ref<boolean>(false)
|
|||
|
|
|
|||
|
|
// ==========================================================================
|
|||
|
|
// 方法
|
|||
|
|
// ==========================================================================
|
|||
|
|
|
|||
|
|
/** 执行搜索 */
|
|||
|
|
async function handleSearch(): Promise<void> {
|
|||
|
|
const query = searchText.value.trim()
|
|||
|
|
if (!query) {
|
|||
|
|
clearSearch()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loading.value = true
|
|||
|
|
searched.value = true
|
|||
|
|
showResults.value = true
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await globalSearch(query)
|
|||
|
|
results.value = response.data.data.items
|
|||
|
|
} catch {
|
|||
|
|
results.value = []
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 输入处理 */
|
|||
|
|
function handleInput(): void {
|
|||
|
|
if (searchText.value.length > 0) {
|
|||
|
|
showResults.value = searched.value
|
|||
|
|
} else {
|
|||
|
|
showResults.value = false
|
|||
|
|
results.value = []
|
|||
|
|
searched.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 清除搜索 */
|
|||
|
|
function clearSearch(): void {
|
|||
|
|
searchText.value = ''
|
|||
|
|
showResults.value = false
|
|||
|
|
results.value = []
|
|||
|
|
searched.value = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 导航到搜索结果 */
|
|||
|
|
function navigateTo(routePath: string): void {
|
|||
|
|
showResults.value = false
|
|||
|
|
router.push(routePath)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 获取结果标签类型 */
|
|||
|
|
function getResultTagType(type: string): string {
|
|||
|
|
switch (type) {
|
|||
|
|
case 'config': return 'primary'
|
|||
|
|
case 'agent': return 'success'
|
|||
|
|
case 'quick_reply': return 'warning'
|
|||
|
|
default: return 'info'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 获取结果类型文本 */
|
|||
|
|
function getResultTypeText(type: string): string {
|
|||
|
|
switch (type) {
|
|||
|
|
case 'config': return '配置'
|
|||
|
|
case 'agent': return '坐席'
|
|||
|
|
case 'quick_reply': return '回复'
|
|||
|
|
default: return type
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
/* 搜索框容器 */
|
|||
|
|
.search-box-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 搜索框 */
|
|||
|
|
.search-box {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
background: var(--bg-primary);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
padding: 0 10px;
|
|||
|
|
gap: 6px;
|
|||
|
|
}
|
|||
|
|
.search-box:focus-within {
|
|||
|
|
border-color: var(--accent);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 输入框 */
|
|||
|
|
.search-input {
|
|||
|
|
background: transparent;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
padding: 6px 0;
|
|||
|
|
font-size: 13px;
|
|||
|
|
outline: none;
|
|||
|
|
width: 180px;
|
|||
|
|
}
|
|||
|
|
.search-input::placeholder {
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 搜索结果 */
|
|||
|
|
.search-results {
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-loading,
|
|||
|
|
.search-empty {
|
|||
|
|
padding: 16px;
|
|||
|
|
text-align: center;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-result-count {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-result-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: background 0.2s;
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
}
|
|||
|
|
.search-result-item:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
.search-result-item:hover {
|
|||
|
|
background: var(--bg-tertiary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-name {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
}
|
|||
|
|
</style>
|