boyuehasfj-vue3-html/src/views/SearchResultsView.vue
2025-06-02 21:36:36 +08:00

783 lines
20 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.

<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>