- 添加 GlobalExceptionHandler 全局异常处理 - 添加 ApiController REST API 控制器 - 更新 WebConfig 跨域配置和 ProductRepository 查询方法 - 新增 monitor/product-detail/profile JSP 视图页面 - 添加 FlashSaleServiceTest 秒杀服务单元测试 - 更新 application.yml 配置
443 lines
11 KiB
Vue
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> |