浏览代码

feat: 优化搜索功能和FAQ内容展示

- 在TheHeader组件中新增搜索功能,支持实时搜索和清空搜索框。
- 更新useSearch组合函数,增强搜索逻辑,支持从JSON文件中动态加载产品数据。
- 在FAQ页面中优化内容展示,支持高亮搜索关键词,提升用户体验。
- 删除不再使用的FAQ文档,简化内容结构,确保信息的准确性和一致性。
- 更新多语言支持,确保搜索提示和结果信息的本地化。
master
lizhuang 1 个月前
父节点
当前提交
a3e4fbaa92

+ 152
- 12
components/TheHeader.vue 查看文件

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

+ 97
- 50
composables/useSearch.ts 查看文件

@@ -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: '公司简介'
}
];

+ 0
- 6
content/faq/en/product-1.md 查看文件

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

+ 0
- 6
content/faq/en/product-2.md 查看文件

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

+ 0
- 6
content/faq/en/purchase-1.md 查看文件

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

+ 5
- 5
content/faq/en/purchase-2.md 查看文件

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

+ 14
- 5
content/faq/en/support-1.md 查看文件

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

在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。

+ 3
- 4
content/faq/en/support-2.md 查看文件

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

+ 0
- 6
content/faq/ja/product-1.md 查看文件

@@ -1,6 +0,0 @@
---
id: 1
category: 製品について
question: 製品の保証期間はどのくらいですか?
answer: 当社の製品は購入日から1年間の保証期間があります。保証期間内に製品に問題が発生した場合は、無償で修理または交換いたします。
---

+ 0
- 6
content/faq/ja/product-2.md 查看文件

@@ -1,6 +0,0 @@
---
id: 2
category: 製品について
question: 製品の取扱説明書はどこで入手できますか?
answer: 製品の取扱説明書は、当社ウェブサイトの製品ページからダウンロードできます。また、製品パッケージにも同梱されています。
---

+ 0
- 6
content/faq/ja/purchase-1.md 查看文件

@@ -1,6 +0,0 @@
---
id: 3
category: 購入について
question: 支払い方法は何がありますか?
answer: クレジットカード、銀行振込、コンビニ決済など、様々な支払い方法をご利用いただけます。詳細はお支払いページをご確認ください。
---

+ 5
- 5
content/faq/ja/purchase-2.md 查看文件

@@ -1,6 +1,6 @@
---
id: 4
category: 購入について
question: 返品・交換は可能ですか?
answer: 商品到着後7日以内であれば、未使用・未開封の商品に限り返品・交換が可能です。詳細は返品・交換ポリシーをご確認ください。
---
title: 可以退货或更换商品吗?
category: 技术支持
sort: 1
---

+ 13
- 4
content/faq/ja/support-1.md 查看文件

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

在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。

+ 4
- 5
content/faq/ja/support-2.md 查看文件

@@ -1,6 +1,5 @@
---
id: 6
category: サポートについて
question: 修理依頼はどのように行えばよいですか?
answer: 修理依頼は、当社ウェブサイトの修理依頼フォームからお申し込みください。修理状況はマイページから確認できます。
---
title: 了解如何获取我们的技术支持服务
category: 技术支持
sort: 1
---

+ 0
- 6
content/faq/zh/product-1.md 查看文件

@@ -1,6 +0,0 @@
---
id: 1
category: 产品相关
question: 产品的保修期是多长时间?
answer: 我们的产品自购买之日起提供1年保修期。如果产品在保修期内出现问题,我们将免费修理或更换。
---

+ 0
- 6
content/faq/zh/product-2.md 查看文件

@@ -1,6 +0,0 @@
---
id: 2
category: 产品相关
question: 在哪里可以获取产品的使用说明书?
answer: 产品说明书可以从我们的网站产品页面下载。此外,产品包装中也附带有纸质说明书。
---

+ 0
- 6
content/faq/zh/purchase-1.md 查看文件

@@ -1,6 +0,0 @@
---
id: 3
category: 购买相关
question: 有哪些支付方式可供选择?
answer: 我们接受信用卡、银行转账、便利店支付等多种支付方式。详细信息请查看支付页面。
---

+ 5
- 5
content/faq/zh/purchase-2.md 查看文件

@@ -1,6 +1,6 @@
---
id: 4
category: 购买相关
question: 可以退货或更换商品吗?
answer: 在收到商品后7天内,未使用和未拆封的商品可以退货或更换。详情请参阅我们的退换货政策。
---
title: 可以退货或更换商品吗?
category: 技术支持
sort: 1
---

+ 5
- 6
content/faq/zh/support-1.md 查看文件

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

在工作时间内,我们将提供即时响应。对于非工作时间提交的问题,我们会在下一个工作日尽快回复。

+ 2
- 3
content/faq/zh/support-2.md 查看文件

@@ -1,6 +1,5 @@
---
id: 6
title: 了解如何获取我们的技术支持服务
category: 技术支持
question: 如何申请维修服务?
answer: 请通过我们网站上的维修申请表格提交维修请求。您可以在个人账户页面查看维修进度。
sort: 1
---

+ 3
- 1
i18n/locales/en.ts 查看文件

@@ -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",

+ 3
- 1
i18n/locales/ja.ts 查看文件

@@ -7,11 +7,13 @@ export default {
contact: "お問い合わせ",
search: "検索",
language: "言語",
searchPlaceholder: "製品、FAQ などを検索",
searchPlaceholder: "製品キーワードを検索",
hotKeywords: "人気のキーワード",
productCategories: "製品カテゴリー",
byUsage: "用途で選ぶ",
clear: "クリア",
noResults: "関連する結果はありません",
searching: "検索中...",
footer: {
productsLinks: {
title: "製品",

+ 3
- 1
i18n/locales/zh.ts 查看文件

@@ -7,11 +7,13 @@ export default {
contact: "联系我们",
search: "搜索",
language: "语言",
searchPlaceholder: "搜索关键字, 产品、FAQ 等",
searchPlaceholder: "搜索产品关键字",
hotKeywords: "热门搜索",
productCategories: "产品分类",
byUsage: "按用途",
clear: "清除",
noResults: "没有找到相关结果",
searching: "搜索中...",
footer: {
productsLinks: {
title: "产品",

+ 96
- 89
pages/faq.vue 查看文件

@@ -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 = "";

正在加载...
取消
保存