boyuehasfj-vue3-html/src/views/SearchResultsView.vue

783 lines
20 KiB
Vue
Raw Normal View History

2025-06-02 21:36:36 +08:00
<script setup lang="ts">
import { ref, onMounted, computed, watchEffect, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import SearchBar from '@/components/SearchBar.vue';
interface SearchResult {
id: number;
formatId: string;
title: string;
pageType: string;
content?: string;
description?: string;
createTime?: string;
updateTime?: string;
}
interface CategorizedResults {
law: SearchResult[];
case: SearchResult[];
form: SearchResult[];
other: SearchResult[];
}
const route = useRoute();
const router = useRouter();
const keyword = ref('');
const searchResults = ref<SearchResult[]>([]);
const categorizedResults = ref<CategorizedResults>({
law: [],
case: [],
form: [],
other: []
});
const isLoading = ref(true);
const errorMessage = ref('');
// 检查是否有结果
const hasResults = computed(() => {
return searchResults.value.length > 0;
});
// 检查各类别是否有结果
const hasLawResults = computed(() => categorizedResults.value.law.length > 0);
const hasCaseResults = computed(() => categorizedResults.value.case.length > 0);
const hasFormResults = computed(() => categorizedResults.value.form.length > 0);
const hasOtherResults = computed(() => categorizedResults.value.other.length > 0);
// 获取结果总数
const totalResults = computed(() => searchResults.value.length);
// 当前选中的分类
const activeCategory = ref('all');
// 监听路由变化从sessionStorage获取搜索结果
watchEffect(() => {
const queryKeyword = route.query.keyword as string;
if (queryKeyword) {
keyword.value = decodeURIComponent(queryKeyword);
loadSearchResults();
}
});
// 加载搜索结果
function loadSearchResults() {
isLoading.value = true;
errorMessage.value = '';
try {
// 从sessionStorage获取搜索结果
const storedKeyword = sessionStorage.getItem('searchKeyword');
const storedResults = sessionStorage.getItem('searchResults');
const storedCategorizedResults = sessionStorage.getItem('categorizedResults');
if (storedKeyword === keyword.value && storedResults) {
// 使用存储的结果
searchResults.value = JSON.parse(storedResults);
if (storedCategorizedResults) {
categorizedResults.value = JSON.parse(storedCategorizedResults);
} else {
// 如果没有存储分类结果,手动分类
categorizeResults();
}
console.log('从sessionStorage加载搜索结果:', searchResults.value.length);
} else {
// 如果没有存储结果或关键词不匹配,重新搜索
console.log('未找到存储的搜索结果或关键词不匹配,重新搜索:', keyword.value);
performSearch();
}
} catch (error) {
console.error('加载搜索结果失败:', error);
errorMessage.value = '加载搜索结果失败';
// 尝试重新搜索
performSearch();
} finally {
isLoading.value = false;
}
}
// 执行搜索
async function performSearch() {
if (!keyword.value || keyword.value.trim() === '') {
errorMessage.value = '请输入搜索关键词';
isLoading.value = false;
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
console.log('执行搜索:', keyword.value);
// 导入searchContent函数
const { searchContent } = await import('@/services/api');
// 调用搜索API设置titleOnly为true仅搜索标题
const response = await searchContent(keyword.value, undefined, true);
if (response && response.code === 200) {
// 处理搜索结果
const results = response.data || [];
searchResults.value = Array.isArray(results) ? results : [];
// 分类结果
categorizeResults();
// 存储到sessionStorage
sessionStorage.setItem('searchKeyword', keyword.value);
sessionStorage.setItem('searchResults', JSON.stringify(searchResults.value));
sessionStorage.setItem('categorizedResults', JSON.stringify(categorizedResults.value));
console.log('搜索成功,结果数量:', searchResults.value.length);
} else {
console.error('搜索失败:', response);
errorMessage.value = response?.msg || '搜索失败,请稍后重试';
}
} catch (error) {
console.error('搜索出错:', error);
errorMessage.value = '搜索服务暂时不可用,请稍后重试';
} finally {
isLoading.value = false;
}
}
// 手动分类结果
function categorizeResults() {
categorizedResults.value = {
law: searchResults.value.filter(item => item.pageType === 'law'),
case: searchResults.value.filter(item => item.pageType === 'case'),
form: searchResults.value.filter(item => item.pageType === 'form'),
other: searchResults.value.filter(item => !['law', 'case', 'form'].includes(item.pageType))
};
}
// 设置活动分类
function setActiveCategory(category: string) {
activeCategory.value = category;
// 滚动到对应的区域
setTimeout(() => {
const element = document.getElementById(`${category}-section`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
// 格式化日期
function formatDate(dateString?: string): string {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric'
});
} catch (e) {
return dateString;
}
}
// 跳转到详情页
function goToDetail(item: SearchResult) {
let url = '/';
switch(item.pageType) {
case 'law':
url = `/show.html?Id=${item.formatId}`;
break;
case 'case':
url = `/showcase.html?Id=${item.formatId}`;
break;
case 'form':
url = `/table.html?Id=${item.formatId}`;
break;
default:
// 如果类型未知尝试使用ID
url = `/show.html?Id=${item.formatId || item.id}`;
}
window.location.href = url;
}
// 监听滚动,高亮当前可见区域
function highlightVisibleSections() {
const sections = document.querySelectorAll('.result-section');
const navItems = document.querySelectorAll('.category-list li');
if (sections.length === 0 || navItems.length === 0) return;
const viewportHeight = window.innerHeight;
let closestSection: Element | null = null;
let closestDistance = Infinity;
sections.forEach((section) => {
const rect = section.getBoundingClientRect();
const distance = Math.abs(rect.top);
if (distance < closestDistance && rect.top <= viewportHeight / 2) {
closestDistance = distance;
closestSection = section;
}
});
if (closestSection && closestSection.id) {
const id = closestSection.id;
const category = id.replace('-section', '');
navItems.forEach((item) => {
const link = item.querySelector('a');
if (link && link.getAttribute('href') === `#${id}`) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// 更新活动分类
if (activeCategory.value !== category) {
activeCategory.value = category;
}
}
}
onMounted(() => {
loadSearchResults();
// 添加滚动监听
window.addEventListener('scroll', highlightVisibleSections);
});
// 组件卸载时移除滚动监听
onUnmounted(() => {
window.removeEventListener('scroll', highlightVisibleSections);
});
</script>
<template>
<div class="search-results-page">
<div class="search-results-container">
<div class="search-header">
<div class="header-content">
<h1>搜索结果</h1>
<div class="search-bar-container">
<SearchBar />
</div>
</div>
</div>
<div class="search-content">
<div class="search-sidebar">
<div class="search-info">
<template v-if="hasResults">
<p>关键词 "<span class="keyword-highlight">{{ keyword }}</span>" 的搜索结果 <span class="result-count">{{ totalResults }}</span> </p>
</template>
<template v-else-if="errorMessage">
<p class="error-message">{{ errorMessage }}</p>
</template>
<template v-else-if="isLoading">
<p>正在加载搜索结果...</p>
</template>
<template v-else>
<p>未找到与 "<span class="keyword-highlight">{{ keyword }}</span>" 相关的内容</p>
</template>
</div>
<div class="search-categories" v-if="hasResults">
<h3>搜索结果分类</h3>
<ul class="category-list">
<li v-if="hasLawResults" :class="{ active: true }">
<a href="#law-section">法律规定 ({{ categorizedResults.law.length }})</a>
</li>
<li v-if="hasCaseResults" :class="{ active: true }">
<a href="#case-section">典型案例 ({{ categorizedResults.case.length }})</a>
</li>
<li v-if="hasFormResults" :class="{ active: true }">
<a href="#form-section">表单下载 ({{ categorizedResults.form.length }})</a>
</li>
<li v-if="hasOtherResults" :class="{ active: true }">
<a href="#other-section">其他内容 ({{ categorizedResults.other.length }})</a>
</li>
</ul>
</div>
</div>
<div class="search-main-content">
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载搜索结果...</p>
</div>
<div v-else class="results-sections">
<!-- 法律规定结果 -->
<section v-if="hasLawResults" id="law-section" class="result-section law-section">
<h2>法律规定 <span class="result-count">({{ categorizedResults.law.length }})</span></h2>
<div class="result-list">
<div v-for="item in categorizedResults.law" :key="item.id" class="result-item" @click="goToDetail(item)">
<div class="result-content">
<h3 class="result-title">{{ item.title }}</h3>
<p v-if="item.description" class="result-description">{{ item.description }}</p>
<div class="result-meta">
<span v-if="item.createTime" class="result-date">{{ formatDate(item.createTime) }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 典型案例结果 -->
<section v-if="hasCaseResults" id="case-section" class="result-section case-section">
<h2>典型案例 <span class="result-count">({{ categorizedResults.case.length }})</span></h2>
<div class="result-list">
<div v-for="item in categorizedResults.case" :key="item.id" class="result-item" @click="goToDetail(item)">
<div class="result-content">
<h3 class="result-title">{{ item.title }}</h3>
<p v-if="item.description" class="result-description">{{ item.description }}</p>
<div class="result-meta">
<span v-if="item.createTime" class="result-date">{{ formatDate(item.createTime) }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 表单下载结果 -->
<section v-if="hasFormResults" id="form-section" class="result-section form-section">
<h2>表单下载 <span class="result-count">({{ categorizedResults.form.length }})</span></h2>
<div class="result-list">
<div v-for="item in categorizedResults.form" :key="item.id" class="result-item" @click="goToDetail(item)">
<div class="result-content">
<h3 class="result-title">{{ item.title }}</h3>
<p v-if="item.description" class="result-description">{{ item.description }}</p>
<div class="result-meta">
<span v-if="item.createTime" class="result-date">{{ formatDate(item.createTime) }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 其他结果 -->
<section v-if="hasOtherResults" id="other-section" class="result-section other-section">
<h2>其他内容 <span class="result-count">({{ categorizedResults.other.length }})</span></h2>
<div class="result-list">
<div v-for="item in categorizedResults.other" :key="item.id" class="result-item" @click="goToDetail(item)">
<div class="result-content">
<h3 class="result-title">{{ item.title }}</h3>
<p v-if="item.description" class="result-description">{{ item.description }}</p>
<div class="result-meta">
<span v-if="item.createTime" class="result-date">{{ formatDate(item.createTime) }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 无结果提示 -->
<div v-if="!hasResults && !isLoading && !errorMessage" class="no-results">
<p class="no-results-title">抱歉未找到与 "<span class="keyword-highlight">{{ keyword }}</span>" 相关的内容</p>
<div class="no-results-suggestions">
<p>建议</p>
<ul>
<li>请检查关键词拼写是否正确</li>
<li>尝试使用其他关键词</li>
<li>尝试使用更通用的关键词</li>
</ul>
</div>
<div class="try-again-section">
<button class="try-again-button" @click="performSearch">重新搜索</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.search-results-page {
background-color: #f5f7fa;
min-height: 100vh;
padding: 20px 0;
}
.search-results-container {
max-width: 1200px;
margin: 0 auto;
}
.search-header {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.header-content {
max-width: 1100px;
margin: 0 auto;
}
.search-header h1 {
font-size: 24px;
margin-bottom: 15px;
color: var(--color-primary);
font-weight: 700;
}
.search-bar-container {
width: 100%;
max-width: 600px;
}
.search-content {
display: flex;
gap: 20px;
}
.search-sidebar {
width: 280px;
flex-shrink: 0;
}
.search-main-content {
flex: 1;
}
.search-info {
background-color: #fff;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
font-size: 15px;
color: #333;
}
.search-categories {
background-color: #fff;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.search-categories h3 {
font-size: 16px;
margin-bottom: 15px;
color: #333;
font-weight: 600;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
}
.category-list li {
margin-bottom: 12px;
}
.category-list a {
display: block;
padding: 8px 12px;
border-radius: 4px;
color: #555;
text-decoration: none;
transition: all 0.2s;
font-size: 15px;
}
.category-list li.active a {
background-color: #f0f7ff;
color: var(--color-primary);
font-weight: 500;
border-left: 3px solid var(--color-primary);
}
.category-list a:hover {
background-color: #f0f7ff;
color: var(--color-primary);
}
.keyword-highlight {
color: var(--color-primary);
font-weight: 600;
}
.result-count {
color: var(--color-primary);
font-weight: 600;
}
.error-message {
color: #d9534f;
font-weight: 500;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 0;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.results-sections {
display: flex;
flex-direction: column;
gap: 20px;
}
.result-section {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
background-color: #fff;
transition: all 0.3s ease;
}
.result-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.result-section h2 {
background-color: #f8f8f8;
padding: 15px 20px;
margin: 0;
font-size: 18px;
border-bottom: 1px solid #e0e0e0;
color: var(--color-primary);
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.result-list {
padding: 0;
}
.result-item {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:last-child {
border-bottom: none;
}
.result-item:hover {
background-color: #f9f9f9;
}
.result-title {
margin: 0 0 10px 0;
font-size: 17px;
color: var(--color-primary);
font-weight: 600;
}
.result-description {
margin: 0 0 10px 0;
font-size: 14px;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.result-meta {
display: flex;
font-size: 12px;
color: #999;
}
.result-date {
margin-right: 15px;
}
.no-results {
padding: 40px 30px;
background-color: #fff;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.no-results-title {
font-size: 18px;
color: #555;
margin-bottom: 20px;
font-weight: 500;
}
.no-results-suggestions {
margin: 20px 0;
}
.no-results p {
margin: 10px 0;
color: #666;
}
.no-results ul {
text-align: left;
display: inline-block;
margin: 10px auto;
padding-left: 20px;
}
.no-results li {
margin: 8px 0;
color: #666;
}
.try-again-section {
margin-top: 25px;
}
.try-again-button {
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 15px;
cursor: pointer;
transition: background-color 0.2s;
font-weight: 500;
}
.try-again-button:hover {
background-color: var(--color-primary-dark);
}
/* 针对不同类型的特殊样式 */
.law-section h2 {
border-left: 4px solid #2c3e50;
}
.case-section h2 {
border-left: 4px solid #e74c3c;
}
.form-section h2 {
border-left: 4px solid #3498db;
}
.other-section h2 {
border-left: 4px solid #f39c12;
}
@media (max-width: 1024px) {
.search-content {
flex-direction: column;
}
.search-sidebar {
width: 100%;
margin-bottom: 20px;
}
.search-categories {
margin-bottom: 0;
}
.category-list {
display: flex;
flex-wrap: wrap;
}
.category-list li {
margin-right: 10px;
margin-bottom: 10px;
}
}
@media (max-width: 768px) {
.search-results-page {
padding: 10px;
}
.search-header {
padding: 15px;
margin-bottom: 15px;
}
.search-header h1 {
font-size: 20px;
}
.search-info {
padding: 12px 15px;
}
.search-categories {
padding: 12px 15px;
}
.result-section h2 {
font-size: 16px;
padding: 12px 15px;
}
.result-item {
padding: 15px;
}
.result-title {
font-size: 16px;
}
.no-results {
padding: 30px 20px;
}
.no-results-title {
font-size: 16px;
}
.category-list li {
margin-right: 5px;
margin-bottom: 5px;
}
.category-list a {
padding: 6px 10px;
font-size: 14px;
}
}
</style>