Files
FlashSaleSystem/flash-sale-frontend/src/components/common/SearchComponent.vue
YoVinchen 989c2741a2 后端功能增强:全局异常处理、API控制器、JSP视图和单元测试
- 添加 GlobalExceptionHandler 全局异常处理
- 添加 ApiController REST API 控制器
- 更新 WebConfig 跨域配置和 ProductRepository 查询方法
- 新增 monitor/product-detail/profile JSP 视图页面
- 添加 FlashSaleServiceTest 秒杀服务单元测试
- 更新 application.yml 配置
2026-03-05 20:30:48 +08:00

443 lines
11 KiB
Vue

<template>
<div class="search-component">
<el-popover
placement="bottom"
:width="600"
trigger="click"
:visible="visible"
@update:visible="val => visible = val"
>
<template #reference>
<el-input
v-model="searchQuery"
placeholder="搜索商品、秒杀活动..."
class="search-input"
@keyup.enter="handleQuickSearch"
@focus="handleFocus"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #suffix>
<el-button
v-if="searchQuery"
text
circle
size="small"
@click.stop="clearSearch"
>
<el-icon><Close /></el-icon>
</el-button>
</template>
</el-input>
</template>
<div class="search-panel">
<!-- 搜索历史 -->
<div v-if="!searchQuery && searchHistory.length > 0" class="search-section">
<div class="section-header">
<span class="title">搜索历史</span>
<el-button text size="small" @click="clearHistory">
清空
</el-button>
</div>
<div class="tag-list">
<el-tag
v-for="item in searchHistory"
:key="item"
closable
@click="selectHistory(item)"
@close="removeHistory(item)"
>
{{ item }}
</el-tag>
</div>
</div>
<!-- 热门搜索 -->
<div v-if="!searchQuery" class="search-section">
<div class="section-header">
<span class="title">热门搜索</span>
</div>
<div class="tag-list">
<el-tag
v-for="item in hotSearches"
:key="item"
@click="selectHot(item)"
>
{{ item }}
</el-tag>
</div>
</div>
<!-- 搜索建议 -->
<div v-if="searchQuery && suggestions.length > 0" class="search-suggestions">
<div class="section-header">
<span class="title">搜索建议</span>
</div>
<div class="suggestion-list">
<div
v-for="item in suggestions"
:key="item.id"
class="suggestion-item"
@click="selectSuggestion(item)"
>
<el-icon :size="16" class="mr-2">
<component :is="item.type === 'product' ? 'ShoppingBag' : 'Lightning'" />
</el-icon>
<div class="content">
<div class="name" v-html="highlightKeyword(item.name)"></div>
<div class="info">
<span class="type">{{ item.type === 'product' ? '商品' : '秒杀' }}</span>
<span class="price">¥{{ item.price }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 高级搜索 -->
<div class="advanced-search">
<el-collapse v-model="activeCollapse">
<el-collapse-item name="advanced">
<template #title>
<span class="text-sm text-blue-500">
<el-icon><Setting /></el-icon>
高级搜索
</span>
</template>
<div class="advanced-form">
<el-form :model="advancedForm" label-width="80px" size="small">
<el-form-item label="商品分类">
<el-select v-model="advancedForm.category" placeholder="选择分类">
<el-option label="全部分类" value="" />
<el-option label="电子产品" value="electronics" />
<el-option label="服装鞋包" value="fashion" />
<el-option label="食品饮料" value="food" />
<el-option label="图书音像" value="books" />
</el-select>
</el-form-item>
<el-form-item label="价格区间">
<div class="flex gap-2">
<el-input-number
v-model="advancedForm.minPrice"
:min="0"
placeholder="最低价"
/>
<span>-</span>
<el-input-number
v-model="advancedForm.maxPrice"
:min="0"
placeholder="最高价"
/>
</div>
</el-form-item>
<el-form-item label="搜索类型">
<el-checkbox-group v-model="advancedForm.types">
<el-checkbox label="product">商品</el-checkbox>
<el-checkbox label="flashsale">秒杀</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAdvancedSearch">
搜索
</el-button>
<el-button @click="resetAdvanced">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { debounce } from 'lodash-es'
const router = useRouter()
const visible = ref(false)
const searchQuery = ref('')
const activeCollapse = ref<string[]>([])
// 搜索历史
const searchHistory = ref<string[]>([])
// 热门搜索
const hotSearches = ref([
'iPhone 15',
'MacBook Pro',
'秒杀活动',
'AirPods',
'限时特价',
'新品上市'
])
// 搜索建议
const suggestions = ref<any[]>([])
// 高级搜索表单
const advancedForm = reactive({
category: '',
minPrice: undefined as number | undefined,
maxPrice: undefined as number | undefined,
types: ['product', 'flashsale']
})
// 从localStorage加载搜索历史
const loadSearchHistory = () => {
const history = localStorage.getItem('searchHistory')
if (history) {
searchHistory.value = JSON.parse(history)
}
}
// 保存搜索历史
const saveSearchHistory = (keyword: string) => {
if (!keyword.trim()) return
// 去重并限制数量
const history = searchHistory.value.filter(item => item !== keyword)
history.unshift(keyword)
searchHistory.value = history.slice(0, 10)
localStorage.setItem('searchHistory', JSON.stringify(searchHistory.value))
}
// 清空搜索历史
const clearHistory = () => {
searchHistory.value = []
localStorage.removeItem('searchHistory')
}
// 移除单个历史
const removeHistory = (item: string) => {
searchHistory.value = searchHistory.value.filter(h => h !== item)
localStorage.setItem('searchHistory', JSON.stringify(searchHistory.value))
}
// 选择历史记录
const selectHistory = (item: string) => {
searchQuery.value = item
handleQuickSearch()
}
// 选择热门搜索
const selectHot = (item: string) => {
searchQuery.value = item
handleQuickSearch()
}
// 选择搜索建议
const selectSuggestion = (item: any) => {
if (item.type === 'product') {
router.push(`/product/${item.id}`)
} else {
router.push(`/flashsale/${item.id}`)
}
visible.value = false
saveSearchHistory(item.name)
}
// 高亮关键词
const highlightKeyword = (text: string) => {
if (!searchQuery.value) return text
const regex = new RegExp(`(${searchQuery.value})`, 'gi')
return text.replace(regex, '<span class="text-red-500 font-bold">$1</span>')
}
// 获取搜索建议
const fetchSuggestions = debounce(async () => {
if (!searchQuery.value.trim()) {
suggestions.value = []
return
}
// 模拟搜索建议
suggestions.value = [
{
id: 1,
type: 'product',
name: `${searchQuery.value} Pro Max`,
price: 8999
},
{
id: 2,
type: 'product',
name: `${searchQuery.value} 标准版`,
price: 5999
},
{
id: 3,
type: 'flashsale',
name: `${searchQuery.value} 限时秒杀`,
price: 4999
}
]
}, 300)
// 快速搜索
const handleQuickSearch = () => {
if (!searchQuery.value.trim()) return
saveSearchHistory(searchQuery.value)
router.push({
path: '/products',
query: { keyword: searchQuery.value }
})
visible.value = false
}
// 高级搜索
const handleAdvancedSearch = () => {
const params: any = {
keyword: searchQuery.value
}
if (advancedForm.category) params.category = advancedForm.category
if (advancedForm.minPrice !== undefined) params.minPrice = advancedForm.minPrice
if (advancedForm.maxPrice !== undefined) params.maxPrice = advancedForm.maxPrice
router.push({
path: '/products',
query: params
})
visible.value = false
saveSearchHistory(searchQuery.value)
}
// 重置高级搜索
const resetAdvanced = () => {
advancedForm.category = ''
advancedForm.minPrice = undefined
advancedForm.maxPrice = undefined
advancedForm.types = ['product', 'flashsale']
}
// 清空搜索
const clearSearch = () => {
searchQuery.value = ''
suggestions.value = []
}
// 处理焦点
const handleFocus = () => {
visible.value = true
}
// 监听搜索输入
watch(searchQuery, () => {
fetchSuggestions()
})
onMounted(() => {
loadSearchHistory()
})
</script>
<style scoped lang="scss">
.search-component {
.search-input {
width: 300px;
@media (max-width: 768px) {
width: 200px;
}
}
}
.search-panel {
.search-section {
margin-bottom: 20px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.el-tag {
cursor: pointer;
&:hover {
background-color: var(--el-color-primary-light-9);
}
}
}
}
.search-suggestions {
.suggestion-list {
.suggestion-item {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
.content {
flex: 1;
.name {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
}
.info {
display: flex;
gap: 10px;
font-size: 12px;
color: #909399;
.type {
padding: 0 6px;
background-color: #f0f0f0;
border-radius: 2px;
}
.price {
color: #f56c6c;
font-weight: 500;
}
}
}
}
}
}
.advanced-search {
margin-top: 20px;
border-top: 1px solid #e4e7ed;
padding-top: 10px;
.advanced-form {
padding: 10px 0;
}
}
}
</style>