2025-11-26 15:42:54 +08:00

687 lines
18 KiB
Vue
Raw 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.

<template>
<!-- 左侧分类导航 -->
<aside class="category-sidebar">
<ul class="category-list">
<li class="category-item special-item">
<img src="../img/hot.svg" alt="热门推荐">
热门推荐 / 活动促销
</li>
<li v-for="category in categories" :key="category.id" class="category-item"
@mouseenter="showProductList(category)" @mouseleave="hideProductList"
:class="{ 'category-item-active': currentCategory && currentCategory.id === category.id }">
<span class="category-icon">
<img :src="category.icon" :alt="category.first_level_name">
</span>
<span class="category-name">{{ category.first_level_name }}</span>
<span class="category-divider">|</span>
<div class="menu-item">
<span v-for="(secondary, index) in category.secondaryClassification" :key="secondary.id">
{{ secondary.second_level_name }}{{ index < category.secondaryClassification.length - 1 ? ' / ' : '' }}
</span>
</div>
<span class="category-arrow"></span>
</li>
</ul>
<transition name="slide-fade">
<div v-loading="loading" element-loading-text="加载中..." element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.8)" class="rightBox" v-if="currentCategory"
@mouseenter="keepProductList" @mouseleave="hideProductList">
<div class="rightBox-content">
<!-- 二级菜单标题 -->
<div class="secondary-menu">
<div v-for="secondary in currentCategory.secondaryClassification" :key="secondary.id" class="secondary-item"
:class="{
active: selectedSecondary === secondary,
'has-children': secondary.thirdClassification && secondary.thirdClassification.length > 0
}"
@mouseenter="selectSecondary(secondary)"
@click="handleSecondaryClick(secondary)">
{{ secondary.second_level_name }}
<span v-if="secondary.thirdClassification && secondary.thirdClassification.length > 0" class="item-arrow"></span>
</div>
</div>
<!-- 三级和四级菜单内容 -->
<div class="menu-content">
<!-- 如果有选中的二级菜单且有三级菜单显示京东风格的分类区域 -->
<div
v-if="selectedSecondary && selectedSecondary.thirdClassification && selectedSecondary.thirdClassification.length > 0"
class="jd-style-menu">
<div v-for="third in selectedSecondary.thirdClassification" :key="third.id" class="category-section">
<!-- 只有当有四级菜单时才显示三级菜单标题 -->
<div v-if="third.product_list && third.product_list.length > 0" class="section-header">
<span class="section-title">{{ third.third_level_name }}</span>
<span class="section-arrow"></span>
</div>
<div class="section-content">
<!-- 如果有四级菜单product_list直接显示所有四级菜单项 -->
<div v-if="third.product_list && third.product_list.length > 0" class="product-grid">
<div @click="goSearch(product)" v-for="(product, index) in third.product_list" :key="product.id"
class="product-tag">
{{ product.first_level_name }}
</div>
</div>
<!-- 如果没有四级菜单将三级菜单项视为四级菜单项显示 -->
<div v-else class="product-grid">
<div @click="openTalk" class="product-tag special-tag">
{{ third.third_level_name }}
</div>
</div>
</div>
</div>
</div>
<!-- 如果没有三级菜单显示二级菜单项 -->
<div v-else-if="selectedSecondary" class="jd-style-menu">
<div class="category-section">
<div class="section-header">
<span class="section-title">{{ selectedSecondary.second_level_name }}</span>
<span class="section-arrow"></span>
</div>
<div class="section-content">
<div class="product-grid">
<div class="product-tag special-tag" @click="goSearch(selectedSecondary)">
{{ selectedSecondary.second_level_name }}
</div>
</div>
</div>
</div>
</div>
<!-- 默认显示所有二级菜单项 -->
<div v-else class="jd-style-menu">
<div v-for="secondary in currentCategory.secondaryClassification" :key="secondary.id"
class="category-section">
<div class="section-header">
<span class="section-title">{{ secondary.second_level_name }}</span>
<span class="section-arrow"></span>
</div>
<div class="section-content">
<div class="product-grid">
<div class="product-tag" @click="goSearch(secondary)">
{{ secondary.second_level_name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
<Talk></Talk>
</aside>
</template>
<script>
import { reqNcMatchMenu, reqSearch } from '@/api/ncmatch';
import { buildDynamicStructure } from './buildNcmatchTree';
import Talk from '@/views/homePage/dialog/talk/index.vue';
import eventBus from '@/utils/eventBus'
export default {
name: 'menuAside',
components: {
Talk
},
data() {
return {
currentCategory: null,
selectedSecondary: null,
hideTimer: null,
categories: [],
loading: false,
keyword: '',
publish_type: '',
activeSection: null
}
},
created() {
this.getCategories();
},
methods: {
performFormalSearch(keyword, type) {
console.log('=== 执行正式搜索开始 ===');
console.log('搜索参数:', { keyword, type });
console.log('当前路由:', this.$route.path);
console.log('事件总线实例:', eventBus);
reqSearch({
url_link: window.location.href,
keyword: keyword,
publish_type: type,
display_page: 'list', // 正式搜索使用 list
current_page: 1,
page_size: 8
}).then(res => {
console.log('正式搜索结果:', res);
// 准备发送事件数据
const eventData = {
type: type,
keyword: keyword,
data: res.data
};
console.log('准备发送事件数据:', eventData);
// 通过事件总线触发搜索事件,让兄弟组件处理结果
try {
eventBus.$emit('search', eventData);
console.log('事件总线事件发送成功');
} catch (error) {
console.error('事件总线事件发送失败:', error);
}
console.log('正式搜索完成,已触发搜索事件');
}).catch(error => {
console.error('正式搜索失败:', error);
this.$message.error('搜索失败,请重试');
});
},
goSearch(product) {
console.log("product", product);
// 添加点击动画效果
const element = event.target;
element.classList.add('click-animation');
setTimeout(() => {
element.classList.remove('click-animation');
}, 300);
if (product.source == 'search') {
// 统一与 search 组件的行为:跳转到 /ncmatchHome/search 并触发正式搜索
const keywordFromItem = product && (product.first_level_name || product.product_name || product.second_level_name)
this.keyword = keywordFromItem || this.keyword || ''
this.publish_type = this.publish_type || '1'
this.$router.push({
path: '/ncmatchHome/search',
query: {
keyword: this.keyword,
publish_type: "1",
display_page: 'list',
current_page: 1,
page_size: 8
}
}).catch(err => {
if (err && err.name !== 'NavigationDuplicated') {
// ignore duplicate
// console.error(err)
}
})
this.performFormalSearch(this.keyword, this.publish_type);
} else {
this.openTalk()
}
},
async openTalk() {
await this.hideProductList('quick')
this.$store.commit('setShowTalk', true);
},
getCategories() {
this.loading = true;
reqNcMatchMenu({ url_link: window.location.href }).then(res => {
this.loading = false;
if (res.status) {
this.categories = buildDynamicStructure(res.data)
console.log("测试", this.categories);
}
})
},
showProductList(category) {
// 清除之前的定时器
if (this.hideTimer) {
clearTimeout(this.hideTimer);
this.hideTimer = null;
}
this.currentCategory = category;
// 自动选中第一个二级菜单
if (category.secondaryClassification && category.secondaryClassification.length > 0) {
this.selectedSecondary = category.secondaryClassification[0];
} else {
this.selectedSecondary = null;
}
},
selectSecondary(secondary) {
this.selectedSecondary = secondary;
},
handleSecondaryClick(secondary) {
this.selectSecondary(secondary);
// 添加点击反馈
if (event) {
const element = event.target;
element.classList.add('click-feedback');
setTimeout(() => {
element.classList.remove('click-feedback');
}, 300);
}
},
keepProductList() {
// 清除隐藏定时器,保持显示
if (this.hideTimer) {
clearTimeout(this.hideTimer);
this.hideTimer = null;
}
},
hideProductList(type) {
if (type === 'quick') {
this.currentCategory = null;
this.selectedSecondary = null;
this.hideTimer = null;
return
}
// 延迟隐藏给用户时间移动到rightBox
this.hideTimer = setTimeout(() => {
this.currentCategory = null;
this.selectedSecondary = null;
this.hideTimer = null;
}, 200);
}
}
}
</script>
<style scoped lang="scss">
.category-sidebar {
position: relative;
background-color: #f8fbfe;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
height: 100%;
border-radius: 10px;
padding: 15px 5px;
width: 100%;
box-sizing: border-box;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
.category-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
transition: all 0.3s;
border-radius: 6px;
margin-bottom: 8px;
position: relative;
&:hover {
color: #2c96fc !important;
background: #e6f3ff !important;
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(44, 150, 252, 0.2);
}
&.category-item-active {
color: #2c96fc !important;
background: #e6f3ff !important;
border-right: 3px solid #2c96fc;
}
&.special-item {
color: #E02020;
font-weight: 600;
img {
margin-right: 8px;
width: 18px;
height: 18px;
}
}
.category-icon {
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
}
}
&:hover .category-icon img {
transform: scale(1.1);
}
.category-name {
font-size: 15px;
font-weight: 500;
white-space: nowrap;
}
.category-divider {
color: #e0e0e0;
margin: 0 4px;
}
.menu-item {
flex: 1;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
color: #666;
}
.category-arrow {
color: #ccc;
font-size: 16px;
font-weight: bold;
transition: transform 0.3s ease, color 0.3s ease;
}
&:hover .category-arrow {
color: #2c96fc;
transform: translateX(3px);
}
}
}
.rightBox {
position: absolute;
left: 100%;
top: 0;
width: 900px;
background: white;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
padding: 20px;
min-height: 400px;
margin-left: 8px;
overflow: visible;
border: 1px solid #e8e8e8;
animation: fadeInScale 0.3s ease;
&::before {
content: '';
position: absolute;
left: -10px;
top: 0;
width: 10px;
height: 100%;
background: transparent;
}
.rightBox-content {
height: 100%;
.secondary-menu {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
.secondary-item {
padding: 8px 16px;
background: #f5f7fa;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
color: #333;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
&:hover,
&.active {
background: linear-gradient(to right, #275AFF, #2EBDFA);
color: #fff;
box-shadow: 0 4px 12px rgba(39, 90, 255, 0.3);
transform: translateY(-2px);
}
&.has-children::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background: #275AFF;
transition: width 0.3s ease;
}
&.active.has-children::after {
width: 80%;
}
&.click-feedback {
animation: subtle-pulse 0.3s ease;
}
.item-arrow {
font-size: 16px;
font-weight: bold;
transition: transform 0.3s ease;
}
&:hover .item-arrow,
&.active .item-arrow {
transform: translateX(3px);
}
}
}
.menu-content {
min-height: 280px;
max-height: 500px;
overflow-y: auto;
padding-right: 5px;
.jd-style-menu {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.category-section {
display: flex;
align-items: flex-start;
margin-bottom: 15px;
.section-header {
display: flex;
align-items: center;
min-width: 120px;
margin-right: 20px;
.section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-right: 6px;
}
.section-arrow {
color: #999;
font-size: 12px;
}
}
.section-content {
flex: 1;
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
.product-tag {
padding: 8px 16px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 20px;
background: #f8f9fa;
position: relative;
overflow: hidden;
border: 1px solid transparent;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
&:hover {
color: #275AFF;
background: #f0f7ff;
border-color: #275AFF;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(39, 90, 255, 0.2);
}
&.click-animation {
animation: subtle-bounce 0.3s ease;
}
}
.special-tag {
color: #275AFF !important;
border: 1px solid #275AFF !important;
background: #f0f7ff !important;
font-weight: 500 !important;
&:hover {
color: #275AFF !important;
background: #e0efff !important;
box-shadow: 0 4px 8px rgba(39, 90, 255, 0.2) !important;
}
}
}
}
}
}
}
}
}
/* 动画定义 */
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.95) translateX(10px);
}
100% {
opacity: 1;
transform: scale(1) translateX(0);
}
}
@keyframes subtle-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
100% {
transform: scale(1);
}
}
@keyframes subtle-bounce {
0%, 20%, 60%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-3px);
}
80% {
transform: translateY(-1px);
}
}
/* 过渡动画样式 */
.slide-fade-enter-active {
transition: all 0.3s ease;
}
.slide-fade-leave-active {
transition: all 0.2s ease;
}
.slide-fade-enter {
opacity: 0;
transform: translateX(10px);
}
.slide-fade-leave-to {
opacity: 0;
transform: translateX(10px);
}
.slide-fade-enter-to,
.slide-fade-leave {
opacity: 1;
transform: translateX(0);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.rightBox {
width: 800px;
}
}
@media (max-width: 992px) {
.rightBox {
width: 700px;
}
}
@media (max-width: 768px) {
.category-sidebar {
display: none;
}
}
</style>