- 在TheHeader组件中新增搜索功能,支持实时搜索和清空搜索框。 - 更新useSearch组合函数,增强搜索逻辑,支持从JSON文件中动态加载产品数据。 - 在FAQ页面中优化内容展示,支持高亮搜索关键词,提升用户体验。 - 删除不再使用的FAQ文档,简化内容结构,确保信息的准确性和一致性。 - 更新多语言支持,确保搜索提示和结果信息的本地化。master
@@ -248,27 +248,91 @@ | |||
</span> | |||
<input | |||
ref="searchInputRef" | |||
v-model="searchQuery" | |||
type="text" | |||
:placeholder=" | |||
t('common.searchPlaceholder') || 'Enter search term...' | |||
" | |||
class="w-full p-3 pl-10 pr-10 rounded bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" | |||
@input="handleSearch" | |||
/> | |||
<!-- Enter Icon --> | |||
<!-- Clear Icon --> | |||
<span | |||
v-if="searchQuery" | |||
class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer group" | |||
@click="clearSearch" | |||
> | |||
<i | |||
class="icon-enter text-gray-400 group-hover:text-blue-400 transition-colors" | |||
></i> | |||
<button | |||
class="flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-blue-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" | |||
:aria-label="t('common.clear')" | |||
tabindex="0" | |||
type="button" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
d="M6 18L18 6M6 6l12 12" | |||
/> | |||
</svg> | |||
</button> | |||
</span> | |||
</div> | |||
<!-- Search results could go here --> | |||
<!-- Search Results --> | |||
<div v-if="isLoading" class="text-gray-400 text-sm"> | |||
{{ t("common.searching") }} | |||
</div> | |||
<div v-if="error" class="text-red-400 text-sm"> | |||
{{ error }} | |||
</div> | |||
<div | |||
v-if="searchResults.length > 0" | |||
class="mt-4 max-h-[60vh] overflow-y-auto pr-2 space-y-4 search-results" | |||
> | |||
<div | |||
v-for="result in searchResults" | |||
:key="result.id" | |||
class="p-3 bg-slate-700/50 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors group" | |||
> | |||
<nuxt-link | |||
:to="`${homePath}products/${result.id}`" | |||
class="block" | |||
@click="closeSearch" | |||
> | |||
<div | |||
class="text-white font-medium mb-1 group-hover:text-blue-400 transition-colors" | |||
v-html="highlightText(result.title, searchQuery)" | |||
></div> | |||
<div | |||
class="text-gray-400 text-sm" | |||
v-html="highlightText(result.description, searchQuery)" | |||
></div> | |||
<div | |||
class="text-gray-500 text-xs mt-1" | |||
v-html="highlightText(result.summary, searchQuery)" | |||
></div> | |||
</nuxt-link> | |||
</div> | |||
</div> | |||
<div | |||
v-else-if="searchQuery && !isLoading" | |||
class="text-gray-400 text-sm mt-4" | |||
> | |||
{{ t("common.noResults") }} | |||
</div> | |||
<!-- Hot Keywords Section --> | |||
<div class="mt-6"> | |||
<h3 class="text-gray-400 text-sm mb-3"> | |||
{{ t("common.hotKeywords") || "热门搜索" }} | |||
{{ t("common.hotKeywords") }} | |||
</h3> | |||
<div class="flex flex-wrap gap-3"> | |||
<button | |||
@@ -289,6 +353,8 @@ | |||
<script setup lang="ts"> | |||
import { ref, computed, watch, nextTick } from "#imports"; | |||
import { useI18n } from "vue-i18n"; | |||
import { useRouter } from "vue-router"; | |||
import { useSearch } from "~/composables/useSearch"; | |||
/** | |||
* 页面头部组件 | |||
@@ -304,6 +370,10 @@ const openDropdown = ref<string | null>(null); | |||
let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay | |||
const route = useRoute(); | |||
const router = useRouter(); | |||
const searchQuery = ref(""); | |||
const { searchResults, isLoading, error, searchProducts, highlightText } = | |||
useSearch(); | |||
// 添加热门关键字 | |||
const hotKeywords = ref(["SSD", "SD", "DDR4"]); | |||
@@ -468,14 +538,12 @@ function closeSearch() { | |||
} | |||
/** | |||
* 搜索热门关键字 (示例) | |||
* @param keyword | |||
* 搜索热门关键字 | |||
* @param keyword 关键词 | |||
*/ | |||
function searchHotKeyword(keyword: string) { | |||
console.log("Searching for hot keyword:", keyword); | |||
// 可以在这里实现填充输入框或直接执行搜索的逻辑 | |||
// 例如: searchInputValue.value = keyword; | |||
closeSearch(); // 点击后可以关闭搜索层 | |||
searchQuery.value = keyword; | |||
handleSearch(); | |||
} | |||
// 监听搜索层状态,打开时自动聚焦输入框 | |||
@@ -503,6 +571,37 @@ function handleMouseLeave() { | |||
}, 150); // 150ms delay | |||
} | |||
// --- End Dropdown Logic --- | |||
// 防抖函数 | |||
const debounce = (fn: Function, delay: number) => { | |||
let timer: NodeJS.Timeout; | |||
return (...args: any[]) => { | |||
clearTimeout(timer); | |||
timer = setTimeout(() => fn(...args), delay); | |||
}; | |||
}; | |||
// 处理搜索输入 | |||
const handleSearch = debounce(async () => { | |||
await searchProducts(searchQuery.value); | |||
}, 300); | |||
// 导航到产品详情页 | |||
const navigateToProduct = (productId: string) => { | |||
const path = | |||
locale.value === "zh" | |||
? `/zh/products/${productId}` | |||
: `/products/${productId}`; | |||
router.push(path); | |||
}; | |||
/** | |||
* 清空搜索 | |||
*/ | |||
const clearSearch = () => { | |||
searchQuery.value = ''; | |||
searchResults.value = []; | |||
}; | |||
</script> | |||
<style lang="scss" scoped> | |||
@@ -577,4 +676,45 @@ header { | |||
.sticky { | |||
position: sticky; | |||
} | |||
/* 搜索结果高亮样式 */ | |||
:deep(.highlight) { | |||
background-color: rgba(59, 130, 246, 0.2); | |||
padding: 0 2px; | |||
border-radius: 2px; | |||
color: #60a5fa; | |||
} | |||
/* 搜索结果动画 */ | |||
.search-results-enter-active, | |||
.search-results-leave-active { | |||
transition: all 0.3s ease; | |||
} | |||
.search-results-enter-from, | |||
.search-results-leave-to { | |||
opacity: 0; | |||
transform: translateY(-10px); | |||
} | |||
/* 搜索结果滚动条样式 */ | |||
.search-results { | |||
&::-webkit-scrollbar { | |||
width: 6px; | |||
} | |||
&::-webkit-scrollbar-track { | |||
background: rgba(255, 255, 255, 0.1); | |||
border-radius: 3px; | |||
} | |||
&::-webkit-scrollbar-thumb { | |||
background: rgba(255, 255, 255, 0.2); | |||
border-radius: 3px; | |||
&:hover { | |||
background: rgba(255, 255, 255, 0.3); | |||
} | |||
} | |||
} | |||
</style> |
@@ -1,15 +1,28 @@ | |||
import { ref, computed } from 'vue'; | |||
import { useErrorHandler } from './useErrorHandler'; | |||
import { useI18n } from 'vue-i18n'; | |||
/** | |||
* 产品数据接口 | |||
*/ | |||
interface Product { | |||
id: string; | |||
title: string; | |||
description: string; | |||
summary: string; | |||
[key: string]: any; | |||
} | |||
/** | |||
* 搜索结果项接口 | |||
*/ | |||
interface SearchResult { | |||
id: number; | |||
id: string; | |||
title: string; | |||
description: string; | |||
type: 'product' | 'faq' | 'page'; | |||
url: string; | |||
summary: string; | |||
matchedField: string; | |||
matchedText: string; | |||
} | |||
/** | |||
@@ -17,84 +30,118 @@ interface SearchResult { | |||
* @returns 搜索相关状态和方法 | |||
*/ | |||
export function useSearch() { | |||
const searchQuery = ref(''); | |||
const searchResults = ref<SearchResult[]>([]); | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const isLoading = ref(false); | |||
const error = ref<string | null>(null); | |||
const { wrapAsync } = useErrorHandler(); | |||
const { locale } = useI18n(); | |||
/** | |||
* 根据查询条件过滤搜索结果 | |||
* 搜索产品数据 | |||
* @param keyword 搜索关键词 | |||
* @returns Promise<void> | |||
*/ | |||
const filteredResults = computed(() => { | |||
if (!searchQuery.value.trim()) return []; | |||
// 简单的模拟搜索实现,实际项目中应替换为真实API | |||
return mockSearchResults.filter(item => | |||
item.title.toLowerCase().includes(searchQuery.value.toLowerCase()) || | |||
item.description.toLowerCase().includes(searchQuery.value.toLowerCase()) | |||
); | |||
}); | |||
const searchProducts = async (keyword: string) => { | |||
if (!keyword.trim()) { | |||
searchResults.value = []; | |||
return; | |||
} | |||
/** | |||
* 执行搜索 | |||
* @param query - 搜索关键词 | |||
*/ | |||
async function search(query: string) { | |||
searchQuery.value = query; | |||
// 在实际项目中,这里应该调用搜索API | |||
await wrapAsync(async () => { | |||
await new Promise(resolve => setTimeout(resolve, 300)); // 模拟API延迟 | |||
searchResults.value = filteredResults.value; | |||
return searchResults.value; | |||
}); | |||
} | |||
isLoading.value = true; | |||
error.value = null; | |||
try { | |||
// 根据当前语言选择对应的JSON文件 | |||
const lang = locale.value; | |||
const products = await fetch(`/data/products-${lang}.json`).then(res => res.json()) as Product[]; | |||
const results: SearchResult[] = []; | |||
// 搜索数据 | |||
products.forEach(product => { | |||
const fields = [ | |||
{ name: 'title', value: product.title }, | |||
{ name: 'description', value: product.description }, | |||
{ name: 'summary', value: product.summary } | |||
]; | |||
fields.forEach(field => { | |||
if (field.value && field.value.toLowerCase().includes(keyword.toLowerCase())) { | |||
results.push({ | |||
id: product.id, | |||
title: product.title, | |||
description: product.description, | |||
summary: product.summary, | |||
matchedField: field.name, | |||
matchedText: field.value | |||
}); | |||
} | |||
}); | |||
}); | |||
// 去重 | |||
searchResults.value = Array.from(new Map(results.map(item => [item.id, item])).values()); | |||
} catch (err) { | |||
error.value = '搜索失败,请稍后重试'; | |||
console.error('Search error:', err); | |||
} finally { | |||
isLoading.value = false; | |||
} | |||
}; | |||
/** | |||
* 清空搜索 | |||
* 高亮显示匹配的文本 | |||
* @param text 原始文本 | |||
* @param keyword 关键词 | |||
* @returns 高亮后的HTML字符串 | |||
*/ | |||
function clearSearch() { | |||
searchQuery.value = ''; | |||
searchResults.value = []; | |||
} | |||
const highlightText = (text: string, keyword: string) => { | |||
if (!keyword || !text) return text; | |||
const regex = new RegExp(`(${keyword})`, 'gi'); | |||
return text.replace(regex, '<span class="highlight">$1</span>'); | |||
}; | |||
return { | |||
searchQuery, | |||
searchResults: computed(() => searchResults.value), | |||
searchResults, | |||
isLoading, | |||
error, | |||
search, | |||
clearSearch | |||
searchProducts, | |||
highlightText | |||
}; | |||
} | |||
// 模拟数据,实际项目中应替换为真实API数据 | |||
const mockSearchResults: SearchResult[] = [ | |||
{ | |||
id: 1, | |||
id: '1', | |||
title: '产品一', | |||
description: '这是产品一的详细描述', | |||
type: 'product', | |||
url: '/products/1' | |||
summary: '这是产品一的详细描述', | |||
matchedField: 'title', | |||
matchedText: '产品一' | |||
}, | |||
{ | |||
id: 2, | |||
id: '2', | |||
title: '产品二', | |||
description: '这是产品二的详细描述', | |||
type: 'product', | |||
url: '/products/2' | |||
summary: '这是产品二的详细描述', | |||
matchedField: 'title', | |||
matchedText: '产品二' | |||
}, | |||
{ | |||
id: 3, | |||
id: '3', | |||
title: '如何使用产品?', | |||
description: '详细介绍产品的使用方法', | |||
type: 'faq', | |||
url: '/faq/1' | |||
summary: '详细介绍产品的使用方法', | |||
matchedField: 'description', | |||
matchedText: '使用方法' | |||
}, | |||
{ | |||
id: 4, | |||
id: '4', | |||
title: '关于我们', | |||
description: '公司简介和历史', | |||
type: 'page', | |||
url: '/about' | |||
summary: '公司简介和历史', | |||
matchedField: 'description', | |||
matchedText: '公司简介' | |||
} | |||
]; |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 1 | |||
category: Products | |||
question: How long is the warranty period for your products? | |||
answer: Our products come with a 1-year warranty from the date of purchase. If any issues occur within the warranty period, we'll repair or replace the product free of charge. | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 2 | |||
category: Products | |||
question: Where can I find the product manual? | |||
answer: Product manuals can be downloaded from our website on the product page. They are also included in the product packaging. | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 3 | |||
category: Purchase | |||
question: What payment methods do you accept? | |||
answer: We accept various payment methods including credit cards, bank transfers, and convenience store payments. Please check our payment page for details. | |||
--- |
@@ -1,6 +1,6 @@ | |||
--- | |||
id: 4 | |||
category: Purchase | |||
question: Can I return or exchange products? | |||
answer: Unused and unopened products can be returned or exchanged within 7 days of receipt. Please refer to our return and exchange policy for details. | |||
--- | |||
title: 可以退货或更换商品吗? | |||
category: 技术支持 | |||
sort: 1 | |||
--- | |||
@@ -1,6 +1,15 @@ | |||
--- | |||
id: 5 | |||
category: Support | |||
question: How can I access technical support? | |||
answer: We offer technical support through email, phone, and online chat. During business hours, we provide immediate assistance. | |||
--- | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持1 | |||
sort: 1 | |||
--- | |||
# 如何获取技术支持? | |||
- 您可以通过以下方式获取技术支持: | |||
1. 电子邮件:`support@example.com` | |||
2. 电话:400-123-4567 | |||
3. 在线聊天:工作日 `9:00-18:00` | |||
在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。 |
@@ -1,6 +1,5 @@ | |||
--- | |||
id: 6 | |||
category: Support | |||
question: How do I request repairs? | |||
answer: Please submit a repair request through the repair request form on our website. You can check the repair status on your account page. | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持 | |||
sort: 1 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 1 | |||
category: 製品について | |||
question: 製品の保証期間はどのくらいですか? | |||
answer: 当社の製品は購入日から1年間の保証期間があります。保証期間内に製品に問題が発生した場合は、無償で修理または交換いたします。 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 2 | |||
category: 製品について | |||
question: 製品の取扱説明書はどこで入手できますか? | |||
answer: 製品の取扱説明書は、当社ウェブサイトの製品ページからダウンロードできます。また、製品パッケージにも同梱されています。 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 3 | |||
category: 購入について | |||
question: 支払い方法は何がありますか? | |||
answer: クレジットカード、銀行振込、コンビニ決済など、様々な支払い方法をご利用いただけます。詳細はお支払いページをご確認ください。 | |||
--- |
@@ -1,6 +1,6 @@ | |||
--- | |||
id: 4 | |||
category: 購入について | |||
question: 返品・交換は可能ですか? | |||
answer: 商品到着後7日以内であれば、未使用・未開封の商品に限り返品・交換が可能です。詳細は返品・交換ポリシーをご確認ください。 | |||
--- | |||
title: 可以退货或更换商品吗? | |||
category: 技术支持 | |||
sort: 1 | |||
--- | |||
@@ -1,6 +1,15 @@ | |||
--- | |||
id: 5 | |||
category: サポートについて | |||
question: 技術サポートはどのように受けられますか? | |||
answer: メール、電話、オンラインチャットなど、様々な方法で技術サポートをご利用いただけます。営業時間内であれば、即時対応いたします。 | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持1 | |||
sort: 1 | |||
--- | |||
# 如何获取技术支持? | |||
- 您可以通过以下方式获取技术支持: | |||
1. 电子邮件:`support@example.com` | |||
2. 电话:400-123-4567 | |||
3. 在线聊天:工作日 `9:00-18:00` | |||
在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。 |
@@ -1,6 +1,5 @@ | |||
--- | |||
id: 6 | |||
category: サポートについて | |||
question: 修理依頼はどのように行えばよいですか? | |||
answer: 修理依頼は、当社ウェブサイトの修理依頼フォームからお申し込みください。修理状況はマイページから確認できます。 | |||
--- | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持 | |||
sort: 1 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 1 | |||
category: 产品相关 | |||
question: 产品的保修期是多长时间? | |||
answer: 我们的产品自购买之日起提供1年保修期。如果产品在保修期内出现问题,我们将免费修理或更换。 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 2 | |||
category: 产品相关 | |||
question: 在哪里可以获取产品的使用说明书? | |||
answer: 产品说明书可以从我们的网站产品页面下载。此外,产品包装中也附带有纸质说明书。 | |||
--- |
@@ -1,6 +0,0 @@ | |||
--- | |||
id: 3 | |||
category: 购买相关 | |||
question: 有哪些支付方式可供选择? | |||
answer: 我们接受信用卡、银行转账、便利店支付等多种支付方式。详细信息请查看支付页面。 | |||
--- |
@@ -1,6 +1,6 @@ | |||
--- | |||
id: 4 | |||
category: 购买相关 | |||
question: 可以退货或更换商品吗? | |||
answer: 在收到商品后7天内,未使用和未拆封的商品可以退货或更换。详情请参阅我们的退换货政策。 | |||
--- | |||
title: 可以退货或更换商品吗? | |||
category: 技术支持 | |||
sort: 1 | |||
--- | |||
@@ -1,16 +1,15 @@ | |||
--- | |||
title: 如何获取技术支持? | |||
description: 了解如何获取我们的技术支持服务 | |||
category: 技术支持 | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持1 | |||
sort: 1 | |||
--- | |||
# 如何获取技术支持? | |||
您可以通过以下方式获取技术支持: | |||
- 您可以通过以下方式获取技术支持: | |||
1. 电子邮件:support@example.com | |||
1. 电子邮件:`support@example.com` | |||
2. 电话:400-123-4567 | |||
3. 在线聊天:工作日 9:00-18:00 | |||
3. 在线聊天:工作日 `9:00-18:00` | |||
在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。 |
@@ -1,6 +1,5 @@ | |||
--- | |||
id: 6 | |||
title: 了解如何获取我们的技术支持服务 | |||
category: 技术支持 | |||
question: 如何申请维修服务? | |||
answer: 请通过我们网站上的维修申请表格提交维修请求。您可以在个人账户页面查看维修进度。 | |||
sort: 1 | |||
--- |
@@ -7,11 +7,13 @@ export default { | |||
contact: "Contact", | |||
search: "Search", | |||
language: "Language", | |||
searchPlaceholder: "Search products, FAQ, etc.", | |||
searchPlaceholder: "Search product keywords", | |||
hotKeywords: "Hot Keywords", | |||
productCategories: "Product Categories", | |||
byUsage: "By Usage", | |||
clear: "Clear", | |||
noResults: "No results found", | |||
searching: "Searching...", | |||
footer: { | |||
productsLinks: { | |||
title: "Products", |
@@ -7,11 +7,13 @@ export default { | |||
contact: "お問い合わせ", | |||
search: "検索", | |||
language: "言語", | |||
searchPlaceholder: "製品、FAQ などを検索", | |||
searchPlaceholder: "製品キーワードを検索", | |||
hotKeywords: "人気のキーワード", | |||
productCategories: "製品カテゴリー", | |||
byUsage: "用途で選ぶ", | |||
clear: "クリア", | |||
noResults: "関連する結果はありません", | |||
searching: "検索中...", | |||
footer: { | |||
productsLinks: { | |||
title: "製品", |
@@ -7,11 +7,13 @@ export default { | |||
contact: "联系我们", | |||
search: "搜索", | |||
language: "语言", | |||
searchPlaceholder: "搜索关键字, 产品、FAQ 等", | |||
searchPlaceholder: "搜索产品关键字", | |||
hotKeywords: "热门搜索", | |||
productCategories: "产品分类", | |||
byUsage: "按用途", | |||
clear: "清除", | |||
noResults: "没有找到相关结果", | |||
searching: "搜索中...", | |||
footer: { | |||
productsLinks: { | |||
title: "产品", |
@@ -50,7 +50,6 @@ | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 右侧FAQ列表 --> | |||
<div class="col-span-1 md:col-span-8"> | |||
<!-- 搜索框 --> | |||
@@ -98,7 +97,7 @@ | |||
> | |||
<div class="text-white text-xl font-medium"> | |||
<template | |||
v-for="(part, i) in highlightKeyword(faq.question)" | |||
v-for="(part, i) in highlightKeyword(faq.title)" | |||
:key="i" | |||
> | |||
<span v-if="typeof part === 'string'">{{ | |||
@@ -118,13 +117,7 @@ | |||
v-if="isFaqExpanded(faq)" | |||
class="mt-4 text-white/80 text-base font-normal leading-relaxed" | |||
> | |||
<template | |||
v-for="(part, i) in highlightKeyword(faq.answer)" | |||
:key="i" | |||
> | |||
<span v-if="typeof part === 'string'">{{ part }}</span> | |||
<component v-else :is="part"></component> | |||
</template> | |||
<ContentRenderer class="text-white" :value="{ body: faq.content }" /> | |||
</div> | |||
</div> | |||
<div | |||
@@ -149,20 +142,18 @@ | |||
* 展示常见问题及其答案 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
import { queryCollection } from '#imports' | |||
import { queryCollection } from "#imports"; | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const { t, locale } = useI18n(); | |||
// FAQ数据 | |||
interface FAQ { | |||
id: string; | |||
category: string; | |||
question: string; | |||
answer: string; | |||
title: string; | |||
description: string; | |||
content: any; // 修改为 any 类型,因为可能是对象或字符串 | |||
sort: number; | |||
id?: string; | |||
} | |||
// 从content目录读取FAQ数据 | |||
@@ -173,78 +164,78 @@ const categoriesList = ref<string[]>([]); | |||
const selectedCategory = ref(""); | |||
// 使用 queryCollection 加载FAQ数据 | |||
const { data: faqData } = await useAsyncData('faqs', async () => { | |||
console.log('Loading FAQ data for locale:', locale.value); | |||
try { | |||
// 使用 queryCollection 加载 FAQ 数据 | |||
const content = await queryCollection("content") | |||
.where("path", "LIKE", `/faq/${locale.value}/%`) | |||
.all(); | |||
if (!content || !Array.isArray(content)) { | |||
console.error('No FAQ content found or invalid format:', content); | |||
const { data: faqData } = await useAsyncData( | |||
"faqs", | |||
async () => { | |||
try { | |||
// 使用 queryCollection 加载 FAQ 数据 | |||
const content = await queryCollection("content") | |||
.where("path", "LIKE", `/faq/${locale.value}/%`) | |||
.all(); | |||
console.log("Raw FAQ content:", content); | |||
if (!content || !Array.isArray(content)) { | |||
console.error("No FAQ content found or invalid format:", content); | |||
return []; | |||
} | |||
// 转换数据格式 | |||
const faqItems = content.map((item: any) => { | |||
console.log("Processing FAQ item:", item); | |||
return { | |||
category: item.meta?.category || "", | |||
title: item.title || "", | |||
content: item.body || "", | |||
sort: item.meta?.sort || 0, | |||
}; | |||
}); | |||
console.log("Processed FAQ items:", faqItems); | |||
return faqItems.sort((a, b) => a.sort - b.sort); | |||
} catch (error) { | |||
console.error("Error loading FAQ content:", error); | |||
return []; | |||
} | |||
console.log('Raw FAQ data:', content); | |||
// 转换数据格式 | |||
const faqItems = content.map((item: any) => { | |||
// 提取路径中的ID | |||
const pathParts = item.path?.split('/'); | |||
const id = pathParts?.[pathParts.length - 1] || ''; | |||
return { | |||
id: id, | |||
category: item.category || '', | |||
question: item.title || '', | |||
answer: item.body || '', | |||
title: item.title || '', | |||
description: item.description || '', | |||
sort: item.sort || 0 | |||
}; | |||
}); | |||
console.log('Processed FAQ items:', faqItems); | |||
// 按 sort 字段排序 | |||
return faqItems.sort((a, b) => a.sort - b.sort); | |||
} catch (error) { | |||
console.error('Error loading FAQ content:', error); | |||
return []; | |||
}, | |||
{ | |||
server: true, | |||
lazy: false, | |||
immediate: true, | |||
watch: [locale], | |||
} | |||
}, { | |||
// 确保数据在构建时生成 | |||
server: true, | |||
lazy: false, | |||
immediate: true, | |||
watch: [locale] | |||
}); | |||
); | |||
// 处理FAQ数据变化 | |||
watchEffect(() => { | |||
if (faqData.value) { | |||
isLoading.value = true; | |||
try { | |||
console.log('Processing FAQ data:', faqData.value); | |||
console.log("Processing FAQ data:", faqData.value); | |||
// 设置分类列表和默认选中的分类 | |||
const allOption: string = | |||
locale.value === "en" ? "All" : locale.value === "zh" ? "全部" : "すべて"; | |||
locale.value === "en" | |||
? "All" | |||
: locale.value === "zh" | |||
? "全部" | |||
: "すべて"; | |||
// 从FAQ数据中提取所有不同的分类 | |||
const uniqueCategories = [ | |||
...new Set(faqData.value.map((faq: FAQ) => faq.category)), | |||
].filter(category => category).sort(); // 过滤掉空分类并排序 | |||
console.log('Unique categories:', uniqueCategories); | |||
] | |||
.filter((category) => category) | |||
.sort(); // 过滤掉空分类并排序 | |||
console.log("Unique categories:", uniqueCategories); | |||
// 设置分类列表和默认选中的分类 | |||
categoriesList.value = [allOption, ...uniqueCategories]; | |||
selectedCategory.value = categoriesList.value[0]; | |||
} catch (err) { | |||
console.error("Error processing FAQ data:", err); | |||
error.value = new Error(t('faq.processError')); | |||
error.value = new Error(t("faq.processError")); | |||
} finally { | |||
isLoading.value = false; | |||
} | |||
@@ -260,22 +251,29 @@ const searchInputRef = ref<HTMLInputElement | null>(null); | |||
// 过滤后的FAQ列表 | |||
const filteredFaqs = computed(() => { | |||
if (!faqData.value) return []; | |||
if (!faqData.value) { | |||
console.log("No FAQ data available"); | |||
return []; | |||
} | |||
let result = faqData.value; | |||
// 分类过滤 | |||
if (selectedCategory.value !== categoriesList.value[0]) { | |||
result = result.filter( | |||
(faq: FAQ) => faq.category === selectedCategory.value | |||
); | |||
} | |||
// 搜索过滤 - 只搜索标题 | |||
if (searchTerm.value.trim()) { | |||
const keyword = searchTerm.value.trim().toLowerCase(); | |||
result = result.filter( | |||
(faq: FAQ) => | |||
faq.question.toLowerCase().includes(keyword) || | |||
faq.answer.toLowerCase().includes(keyword) | |||
); | |||
result = result.filter((faq: FAQ) => { | |||
const title = String(faq.title || "").toLowerCase(); | |||
return title.includes(keyword); | |||
}); | |||
} | |||
return result; | |||
}); | |||
@@ -284,15 +282,20 @@ const filteredFaqs = computed(() => { | |||
* @param text 原始文本 | |||
* @returns 高亮后的VNode数组 | |||
*/ | |||
function highlightKeyword(text: string): (string | any)[] { | |||
function highlightKeyword(text: any): (string | any)[] { | |||
const keyword = searchTerm.value.trim(); | |||
if (!keyword) return [text]; | |||
// 确保 text 是字符串 | |||
const textStr = typeof text === "string" ? text : String(text?.body || ""); | |||
// 构建正则,忽略大小写,转义特殊字符 | |||
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |||
const reg = new RegExp(escaped, "gi"); | |||
const parts = text.split(reg); | |||
const matches = text.match(reg); | |||
if (!matches) return [text]; | |||
const parts = textStr.split(reg); | |||
const matches = textStr.match(reg); | |||
if (!matches) return [textStr]; | |||
// 组装高亮 | |||
const result: (string | any)[] = []; | |||
parts.forEach((part, i) => { | |||
@@ -316,7 +319,7 @@ function highlightKeyword(text: string): (string | any)[] { | |||
* @returns string 唯一标识 | |||
*/ | |||
function generateFaqKey(faq: FAQ): string { | |||
return `${faq.category}-${faq.id}`; | |||
return `${faq.category}-${faq.title}`; | |||
} | |||
/** | |||
@@ -326,7 +329,7 @@ function generateFaqKey(faq: FAQ): string { | |||
*/ | |||
function toggleFaq(faq: FAQ): void { | |||
if (!faq) return; | |||
const faqKey = generateFaqKey(faq); | |||
// 如果点击的是当前展开的FAQ,则关闭它 | |||
if (expandedFaqKey.value === faqKey) { | |||
@@ -355,17 +358,21 @@ function handleCategoryFilter(category: string) { | |||
} | |||
// 自动展开匹配项 | |||
watch([filteredFaqs, searchTerm], ([faqs, keyword]: [FAQ[], string]) => { | |||
if (!faqs?.length) return; | |||
if (keyword.trim()) { | |||
// 搜索时展开第一个匹配项 | |||
expandedFaqKey.value = faqs[0] ? generateFaqKey(faqs[0]) : null; | |||
} else { | |||
// 清除搜索时关闭所有展开项 | |||
expandedFaqKey.value = null; | |||
} | |||
}, { deep: true }); | |||
watch( | |||
[filteredFaqs, searchTerm], | |||
([faqs, keyword]: [FAQ[], string]) => { | |||
if (!faqs?.length) return; | |||
if (keyword.trim()) { | |||
// 搜索时展开第一个匹配项 | |||
expandedFaqKey.value = faqs[0] ? generateFaqKey(faqs[0]) : null; | |||
} else { | |||
// 清除搜索时关闭所有展开项 | |||
expandedFaqKey.value = null; | |||
} | |||
}, | |||
{ deep: true } | |||
); | |||
function clearSearch() { | |||
searchTerm.value = ""; |