chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
+243
View File
@@ -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>