Files

244 lines
6.0 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
=============================================================================
企微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>