chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
企微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>
|
||||
Reference in New Issue
Block a user