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>
|