- 添加支持页面,提供技术支持服务入口,包含FAQ和联系信息。 - 新增FAQ内容组件,支持分类和搜索功能,提升用户体验。 - 更新国际化文件,增加支持页面相关内容的多语言支持。 - 删除旧的FAQ页面,整合至新的支持页面中,优化代码结构。master
<template> | |||||
<div class="faq-content"> | |||||
<div v-if="isLoading" class="flex justify-center py-12"> | |||||
<div class="animate-spin h-8 w-8 border-2 border-blue-400 border-t-transparent rounded-full"></div> | |||||
</div> | |||||
<div v-else class="p-8"> | |||||
<!-- 分类和搜索区域 --> | |||||
<div class="mb-8 space-y-6"> | |||||
<!-- 分类选择 --> | |||||
<div class="flex flex-wrap gap-2"> | |||||
<button | |||||
v-for="category in categoriesList" | |||||
:key="category" | |||||
@click="handleCategoryFilter(category)" | |||||
class="px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200 border" | |||||
:class="{ | |||||
'bg-blue-600 text-white border-blue-600': selectedCategory === category, | |||||
'bg-gray-700 text-gray-300 border-gray-600 hover:bg-gray-600': selectedCategory !== category, | |||||
}" | |||||
> | |||||
{{ category }} | |||||
</button> | |||||
</div> | |||||
<!-- 搜索框 --> | |||||
<div class="relative max-w-md"> | |||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |||||
<svg class="h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<input | |||||
v-model="searchTerm" | |||||
type="search" | |||||
:placeholder="t('faq.searchPlaceholder')" | |||||
class="w-full pl-10 pr-10 py-3 border border-gray-600 rounded-md text-white bg-gray-700 placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none" | |||||
/> | |||||
<button | |||||
v-if="searchTerm" | |||||
@click="clearSearch" | |||||
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-300" | |||||
> | |||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<!-- FAQ列表 --> | |||||
<div class="space-y-3"> | |||||
<div | |||||
v-for="faq in filteredFaqs" | |||||
:key="generateFaqKey(faq)" | |||||
class="border border-gray-600 rounded-lg overflow-hidden" | |||||
> | |||||
<div | |||||
class="flex items-center justify-between p-6 cursor-pointer hover:bg-gray-700 transition-colors duration-200" | |||||
@click="toggleFaq(faq)" | |||||
> | |||||
<div class="text-white font-medium flex-1 text-left"> | |||||
<template | |||||
v-for="(part, i) in highlightKeyword(faq.title)" | |||||
:key="i" | |||||
> | |||||
<span v-if="typeof part === 'string'">{{ part }}</span> | |||||
<component v-else :is="part"></component> | |||||
</template> | |||||
</div> | |||||
<div | |||||
class="text-gray-400 transition-transform duration-200 ml-4 flex-shrink-0" | |||||
:class="{ 'rotate-180': isFaqExpanded(faq) }" | |||||
> | |||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> | |||||
</svg> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="isFaqExpanded(faq)" | |||||
class="px-6 pb-6 border-t border-gray-600 bg-gray-750" | |||||
> | |||||
<div class="pt-6"> | |||||
<ContentRenderer | |||||
class="prose prose-invert prose-sm max-w-none" | |||||
:value="{ body: faq.content }" | |||||
v-highlight="searchTerm.trim()" | |||||
/> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="filteredFaqs.length === 0" | |||||
class="text-center text-gray-400 py-16" | |||||
> | |||||
<div class="mb-4"> | |||||
<svg class="mx-auto h-12 w-12 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |||||
</svg> | |||||
</div> | |||||
<p class="text-lg font-medium text-white mb-2">{{ t("faq.noResults") }}</p> | |||||
<p class="text-sm text-gray-500">尝试调整搜索条件或选择不同的分类</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* FAQ内容组件 | |||||
* 专业的常见问题解答展示组件 | |||||
*/ | |||||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||||
import { queryCollection } from "#imports"; | |||||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||||
const { t, locale } = useI18n(); | |||||
// FAQ数据接口 | |||||
interface FAQ { | |||||
category: string; | |||||
title: string; | |||||
content: any; | |||||
sort: number; | |||||
id?: string; | |||||
} | |||||
// 响应式数据 | |||||
const faqs = ref<FAQ[]>([]); | |||||
const categoriesList = ref<string[]>([]); | |||||
const selectedCategory = ref(""); | |||||
const expandedFaqKeys = ref<Set<string>>(new Set()); | |||||
const searchTerm = ref(""); | |||||
// 使用 queryCollection 加载FAQ数据 | |||||
const { data: faqData } = await useAsyncData( | |||||
"faqs", | |||||
async () => { | |||||
try { | |||||
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); | |||||
return []; | |||||
} | |||||
const faqItems = content.map((item: any) => { | |||||
return { | |||||
category: item.meta?.category || "", | |||||
title: item.title || "", | |||||
content: item.body || "", | |||||
sort: item.meta?.sort || 0, | |||||
}; | |||||
}); | |||||
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], | |||||
} | |||||
); | |||||
// 处理FAQ数据变化 | |||||
watchEffect(() => { | |||||
if (faqData.value) { | |||||
isLoading.value = true; | |||||
try { | |||||
const allOption: string = | |||||
locale.value === "en" | |||||
? "All" | |||||
: locale.value === "zh" | |||||
? "全部" | |||||
: "すべて"; | |||||
const uniqueCategories = [ | |||||
...new Set(faqData.value.map((faq: FAQ) => faq.category)), | |||||
] | |||||
.filter((category) => category) | |||||
.sort(); | |||||
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")); | |||||
} finally { | |||||
isLoading.value = false; | |||||
} | |||||
} | |||||
}); | |||||
// 过滤后的FAQ列表 | |||||
const filteredFaqs = computed(() => { | |||||
if (!faqData.value) { | |||||
return []; | |||||
} | |||||
let filtered = [...faqData.value]; | |||||
// 按分类过滤 | |||||
const allOption: string = | |||||
locale.value === "en" | |||||
? "All" | |||||
: locale.value === "zh" | |||||
? "全部" | |||||
: "すべて"; | |||||
if (selectedCategory.value && selectedCategory.value !== allOption) { | |||||
filtered = filtered.filter((faq) => faq.category === selectedCategory.value); | |||||
} | |||||
// 按搜索词过滤 | |||||
if (searchTerm.value.trim()) { | |||||
const search = searchTerm.value.trim().toLowerCase(); | |||||
filtered = filtered.filter((faq) => | |||||
faq.title.toLowerCase().includes(search) | |||||
); | |||||
} | |||||
return filtered; | |||||
}); | |||||
/** | |||||
* 生成FAQ的唯一标识 | |||||
*/ | |||||
const generateFaqKey = (faq: FAQ): string => { | |||||
return `${faq.category}-${faq.title}-${faq.sort}`; | |||||
}; | |||||
/** | |||||
* 检查FAQ是否展开 | |||||
*/ | |||||
const isFaqExpanded = (faq: FAQ): boolean => { | |||||
return expandedFaqKeys.value.has(generateFaqKey(faq)); | |||||
}; | |||||
/** | |||||
* 切换FAQ展开状态 | |||||
*/ | |||||
const toggleFaq = (faq: FAQ): void => { | |||||
const key = generateFaqKey(faq); | |||||
if (expandedFaqKeys.value.has(key)) { | |||||
expandedFaqKeys.value.delete(key); | |||||
} else { | |||||
expandedFaqKeys.value.add(key); | |||||
} | |||||
}; | |||||
/** | |||||
* 处理分类过滤 | |||||
*/ | |||||
const handleCategoryFilter = (category: string): void => { | |||||
selectedCategory.value = category; | |||||
// 清空展开状态 | |||||
expandedFaqKeys.value.clear(); | |||||
}; | |||||
/** | |||||
* 清除搜索 | |||||
*/ | |||||
const clearSearch = (): void => { | |||||
searchTerm.value = ""; | |||||
}; | |||||
/** | |||||
* 高亮关键词 | |||||
*/ | |||||
const highlightKeyword = (text: string) => { | |||||
if (!searchTerm.value.trim()) { | |||||
return [text]; | |||||
} | |||||
const keyword = searchTerm.value.trim(); | |||||
const regex = new RegExp(`(${keyword})`, "gi"); | |||||
const parts = text.split(regex); | |||||
return parts.map((part) => { | |||||
if (part.toLowerCase() === keyword.toLowerCase()) { | |||||
return h("mark", { class: "bg-yellow-500/30 text-yellow-200 px-1 rounded" }, part); | |||||
} | |||||
return part; | |||||
}); | |||||
}; | |||||
// 自定义指令:高亮搜索结果 | |||||
const vHighlight = { | |||||
mounted(el: HTMLElement, binding: { value: string }) { | |||||
highlightText(el, binding.value); | |||||
}, | |||||
updated(el: HTMLElement, binding: { value: string }) { | |||||
highlightText(el, binding.value); | |||||
}, | |||||
}; | |||||
/** | |||||
* 高亮文本中的关键词 | |||||
*/ | |||||
const highlightText = (el: HTMLElement, keyword: string): void => { | |||||
if (!keyword || !keyword.trim()) { | |||||
return; | |||||
} | |||||
const walker = document.createTreeWalker( | |||||
el, | |||||
NodeFilter.SHOW_TEXT, | |||||
null | |||||
); | |||||
const nodes: Text[] = []; | |||||
let node: Text | null; | |||||
while ((node = walker.nextNode() as Text)) { | |||||
nodes.push(node); | |||||
} | |||||
nodes.forEach((textNode) => { | |||||
const parent = textNode.parentNode; | |||||
if (!parent || parent.nodeName === "MARK") return; | |||||
const text = textNode.textContent || ""; | |||||
const regex = new RegExp(`(${keyword.trim()})`, "gi"); | |||||
if (regex.test(text)) { | |||||
const fragment = document.createDocumentFragment(); | |||||
const parts = text.split(regex); | |||||
parts.forEach((part) => { | |||||
if (part.toLowerCase() === keyword.trim().toLowerCase()) { | |||||
const mark = document.createElement("mark"); | |||||
mark.className = "bg-yellow-500/30 text-yellow-200 px-1 rounded"; | |||||
mark.textContent = part; | |||||
fragment.appendChild(mark); | |||||
} else if (part) { | |||||
fragment.appendChild(document.createTextNode(part)); | |||||
} | |||||
}); | |||||
parent.replaceChild(fragment, textNode); | |||||
} | |||||
}); | |||||
}; | |||||
</script> | |||||
<style scoped> | |||||
/* 过渡效果 */ | |||||
.transition-colors { | |||||
transition-property: color, background-color, border-color; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.transition-transform { | |||||
transition-property: transform; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.duration-200 { | |||||
transition-duration: 200ms; | |||||
} | |||||
/* 悬停效果 */ | |||||
.hover\:bg-gray-700:hover { | |||||
background-color: #374151; | |||||
} | |||||
.hover\:bg-gray-600:hover { | |||||
background-color: #4b5563; | |||||
} | |||||
.hover\:text-gray-300:hover { | |||||
color: #d1d5db; | |||||
} | |||||
/* 焦点样式 */ | |||||
input:focus { | |||||
box-shadow: 0 0 0 1px #3b82f6; | |||||
} | |||||
/* 高亮样式 */ | |||||
mark { | |||||
background-color: rgba(234, 179, 8, 0.3); | |||||
color: #fef3c7; | |||||
padding: 2px 4px; | |||||
border-radius: 4px; | |||||
} | |||||
/* Prose样式覆盖 - 深色主题 */ | |||||
:deep(.prose) { | |||||
color: #d1d5db; | |||||
max-width: none; | |||||
} | |||||
:deep(.prose h1), | |||||
:deep(.prose h2), | |||||
:deep(.prose h3), | |||||
:deep(.prose h4), | |||||
:deep(.prose h5), | |||||
:deep(.prose h6) { | |||||
color: #f9fafb; | |||||
font-weight: 600; | |||||
} | |||||
:deep(.prose p) { | |||||
margin-top: 1rem; | |||||
margin-bottom: 1rem; | |||||
line-height: 1.625; | |||||
color: #d1d5db; | |||||
} | |||||
:deep(.prose ul), | |||||
:deep(.prose ol) { | |||||
margin-top: 1rem; | |||||
margin-bottom: 1rem; | |||||
} | |||||
:deep(.prose li) { | |||||
margin-top: 0.5rem; | |||||
margin-bottom: 0.5rem; | |||||
color: #d1d5db; | |||||
} | |||||
:deep(.prose strong) { | |||||
color: #f9fafb; | |||||
font-weight: 600; | |||||
} | |||||
:deep(.prose code) { | |||||
color: #f9fafb; | |||||
background-color: #374151; | |||||
padding: 0.125rem 0.25rem; | |||||
border-radius: 0.25rem; | |||||
font-size: 0.875em; | |||||
} | |||||
:deep(.prose a) { | |||||
color: #60a5fa; | |||||
text-decoration: underline; | |||||
} | |||||
:deep(.prose a:hover) { | |||||
color: #93c5fd; | |||||
} | |||||
/* 自定义gray-750背景色 */ | |||||
.bg-gray-750 { | |||||
background-color: #2d3748; | |||||
} | |||||
/* 响应式优化 */ | |||||
@media (max-width: 768px) { | |||||
.flex-wrap { | |||||
gap: 0.5rem; | |||||
} | |||||
.px-6 { | |||||
padding-left: 1rem; | |||||
padding-right: 1rem; | |||||
} | |||||
} | |||||
</style> |
}, | }, | ||||
{ | { | ||||
label: "common.footer.websiteLinks.faq", | label: "common.footer.websiteLinks.faq", | ||||
path: locale.value === defaultLocale ? "/faq" : `/${locale.value}/faq`, | |||||
path: locale.value === defaultLocale ? "/support" : `/${locale.value}/support`, | |||||
}, | }, | ||||
{ | { | ||||
label: "common.footer.websiteLinks.about", | label: "common.footer.websiteLinks.about", |
<div | <div | ||||
@mouseenter="handleMouseEnter(item.label)" | @mouseenter="handleMouseEnter(item.label)" | ||||
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md" | class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md" | ||||
:class="[route.path.startsWith(item.pathPrefix) ? 'font-bold opacity-100 bg-white/15' : '']" | |||||
:class="[ | |||||
route.path.startsWith(item.pathPrefix) | |||||
? 'font-bold opacity-100 bg-white/15' | |||||
: '', | |||||
]" | |||||
> | > | ||||
<span>{{ t(item.label) }}</span> | <span>{{ t(item.label) }}</span> | ||||
</div> | </div> | ||||
class="fixed left-0 top-[70px] w-screen bg-slate-900/95 backdrop-blur-[50px] border-t border-b border-slate-700/20 shadow-2xl z-10" | class="fixed left-0 top-[70px] w-screen bg-slate-900/95 backdrop-blur-[50px] border-t border-b border-slate-700/20 shadow-2xl z-10" | ||||
> | > | ||||
<!-- 内容居中容器 --> | <!-- 内容居中容器 --> | ||||
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 py-12"> | |||||
<div | |||||
class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 py-12" | |||||
> | |||||
<!-- 主内容网格 --> | <!-- 主内容网格 --> | ||||
<div> | <div> | ||||
<!-- 产品分类列 (原有的分类逻辑和数据) --> | <!-- 产品分类列 (原有的分类逻辑和数据) --> | ||||
<div> | <div> | ||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-4"> | |||||
<h3 | |||||
class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-4" | |||||
> | |||||
{{ t("common.productCategories") }} | {{ t("common.productCategories") }} | ||||
</h3> | </h3> | ||||
<div class="flex gap-x-32 gap-y-8"> | |||||
<div class="flex gap-x-40 gap-y-8"> | |||||
<!-- 企业用户产品组 --> | <!-- 企业用户产品组 --> | ||||
<div> | <div> | ||||
<h4 class="text-base font-semibold text-white mb-3"> | |||||
<h4 | |||||
class="text-lg font-semibold text-white mb-6" | |||||
> | |||||
<nuxt-link | <nuxt-link | ||||
:to="`${homePath}products?audiences=1`" | :to="`${homePath}products?audiences=1`" | ||||
class="flex items-center gap-2 hover:text-cyan-400 transition-colors" | class="flex items-center gap-2 hover:text-cyan-400 transition-colors" | ||||
</h4> | </h4> | ||||
<ul class="space-y-2"> | <ul class="space-y-2"> | ||||
<li | <li | ||||
v-for="link in getGroupedItems(item.children[0].items).enterprise" | |||||
v-for="link in getGroupedItems( | |||||
item.children[0].items | |||||
).enterprise" | |||||
:key="link.path" | :key="link.path" | ||||
> | > | ||||
<nuxt-link | <nuxt-link | ||||
> | > | ||||
{{ t(link.label) }} | {{ t(link.label) }} | ||||
</nuxt-link> | </nuxt-link> | ||||
<ul v-if="link.tags && link.tags.length" class="mt-1"> | |||||
<li v-for="tag in link.tags" :key="tag"> | |||||
<ul | |||||
v-if="link.tags && link.tags.length" | |||||
class="mt-2" | |||||
> | |||||
<li | |||||
v-for="tag in link.tags" | |||||
:key="tag" | |||||
class="mt-4 mb-4" | |||||
> | |||||
<nuxt-link | <nuxt-link | ||||
:to="`${link.path}&tag=${tag}`" | :to="`${link.path}&tag=${tag}`" | ||||
@click.stop="handleMouseLeave" | @click.stop="handleMouseLeave" | ||||
<!-- 个人用户产品组 --> | <!-- 个人用户产品组 --> | ||||
<div class="flex-1"> | <div class="flex-1"> | ||||
<h4 class="text-base font-semibold text-white mb-3"> | |||||
<h4 | |||||
class="text-lg font-semibold text-white mb-6" | |||||
> | |||||
<nuxt-link | <nuxt-link | ||||
:to="`${homePath}products?audiences=0`" | :to="`${homePath}products?audiences=0`" | ||||
class="flex items-center gap-2 hover:text-cyan-400 transition-colors" | class="flex items-center gap-2 hover:text-cyan-400 transition-colors" | ||||
<span>{{ t("home.personal.title") }}</span> | <span>{{ t("home.personal.title") }}</span> | ||||
</nuxt-link> | </nuxt-link> | ||||
</h4> | </h4> | ||||
<ul class="flex gap-12"> | |||||
<ul class="flex gap-24"> | |||||
<li | <li | ||||
v-for="link in getGroupedItems(item.children[0].items).personal" | |||||
v-for="link in getGroupedItems( | |||||
item.children[0].items | |||||
).personal" | |||||
:key="link.path" | :key="link.path" | ||||
> | > | ||||
<nuxt-link | <nuxt-link | ||||
> | > | ||||
{{ t(link.label) }} | {{ t(link.label) }} | ||||
</nuxt-link> | </nuxt-link> | ||||
<ul v-if="link.tags && link.tags.length" class="mt-1"> | |||||
<li v-for="tag in link.tags" :key="tag"> | |||||
<ul | |||||
v-if="link.tags && link.tags.length" | |||||
class="mt-2" | |||||
> | |||||
<li | |||||
v-for="tag in link.tags" | |||||
:key="tag" | |||||
class="mt-4 mb-4" | |||||
> | |||||
<nuxt-link | <nuxt-link | ||||
:to="`${link.path}&tag=${tag}`" | :to="`${link.path}&tag=${tag}`" | ||||
@click.stop="handleMouseLeave" | @click.stop="handleMouseLeave" | ||||
}, | }, | ||||
], | ], | ||||
}, | }, | ||||
{ label: "common.faq", path: `${prefix}/faq` }, | |||||
{ label: "support.title", path: `${prefix}/support` }, | |||||
{ label: "common.about", path: `${prefix}/about` }, | { label: "common.about", path: `${prefix}/about` }, | ||||
{ label: "common.contact", path: `${prefix}/contact` }, | { label: "common.contact", path: `${prefix}/contact` }, | ||||
]; | ]; | ||||
/* 添加下拉菜单背景渐变 */ | /* 添加下拉菜单背景渐变 */ | ||||
.bg-slate-900\/95 { | .bg-slate-900\/95 { | ||||
background: linear-gradient(to bottom, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.98)); | |||||
background: linear-gradient( | |||||
to bottom, | |||||
rgba(15, 23, 42, 0.95), | |||||
rgba(15, 23, 42, 0.98) | |||||
); | |||||
} | } | ||||
/* 响应式布局 */ | /* 响应式布局 */ | ||||
} | } | ||||
} | } | ||||
</style> | </style> | ||||
category: "Category", | category: "Category", | ||||
noResults: "No results found", | noResults: "No results found", | ||||
clearSearch: "Clear search", | clearSearch: "Clear search", | ||||
processError: "Error processing FAQ data", | |||||
}, | |||||
support: { | |||||
title: "Support", | |||||
subtitle: "We provide comprehensive technical support services to help you solve various product-related issues", | |||||
description: "Get professional technical support services, including FAQ, product manuals, driver downloads, and more", | |||||
keywords: "Hanye, technical support, customer service, FAQ, product manuals, driver downloads", | |||||
viewMore: "View More", | |||||
contactUs: "Contact Us", | |||||
viewDocs: "View Documentation", | |||||
download: "Download", | |||||
startChat: "Start Chat", | |||||
comingSoon: "Coming Soon", | |||||
faq: { | |||||
title: "FAQ", | |||||
description: "Browse the most common product questions and answers" | |||||
}, | |||||
contact: { | |||||
title: "Contact Us", | |||||
description: "Get in touch with our technical support team directly" | |||||
}, | |||||
docs: { | |||||
title: "Technical Documentation", | |||||
description: "Access detailed technical documents and user guides" | |||||
}, | |||||
manuals: { | |||||
title: "Product Manuals", | |||||
description: "Download product manuals and installation guides" | |||||
}, | |||||
drivers: { | |||||
title: "Driver Downloads", | |||||
description: "Get the latest product drivers and software" | |||||
}, | |||||
chat: { | |||||
title: "Live Chat", | |||||
description: "Chat with our technical experts in real-time" | |||||
}, | |||||
backToSupport: "Back to Support" | |||||
}, | }, | ||||
about: { | about: { | ||||
title: "About Us", | title: "About Us", | ||||
title: "Technical Support & Quality Management", | title: "Technical Support & Quality Management", | ||||
description: "We aim to build long-term relationships of trust through professional technical support, consulting, and comprehensive after-sales service.", | description: "We aim to build long-term relationships of trust through professional technical support, consulting, and comprehensive after-sales service.", | ||||
support: { | support: { | ||||
title: "Technical Support", | |||||
title: "Support", | |||||
item1: "Professional technical consultation", | item1: "Professional technical consultation", | ||||
item2: "Optimal solution proposals", | item2: "Optimal solution proposals", | ||||
item3: "Technical training", | item3: "Technical training", |
category: "カテゴリー", | category: "カテゴリー", | ||||
noResults: "該当する質問はありません", | noResults: "該当する質問はありません", | ||||
clearSearch: "検索をクリア", | clearSearch: "検索をクリア", | ||||
processError: "FAQ データの処理中にエラーが発生しました", | |||||
}, | |||||
support: { | |||||
title: "技術サポート", | |||||
subtitle: "お客様の製品利用に関する様々な問題を解決するため、包括的な技術サポートサービスを提供しています", | |||||
description: "よくある質問、製品マニュアル、ドライバーダウンロードなど、専門的な技術サポートサービスをご利用ください", | |||||
keywords: "Hanye, 技術サポート, カスタマーサービス, FAQ, 製品マニュアル, ドライバーダウンロード", | |||||
viewMore: "詳細を見る", | |||||
contactUs: "お問い合わせ", | |||||
viewDocs: "ドキュメントを見る", | |||||
download: "ダウンロード", | |||||
startChat: "チャットを開始", | |||||
comingSoon: "近日公開", | |||||
faq: { | |||||
title: "FAQ", | |||||
description: "最も一般的な製品の質問と回答をご覧ください" | |||||
}, | |||||
contact: { | |||||
title: "お問い合わせ", | |||||
description: "技術サポートチームに直接お問い合わせください" | |||||
}, | |||||
docs: { | |||||
title: "技術ドキュメント", | |||||
description: "詳細な技術ドキュメントとユーザーガイドをご覧ください" | |||||
}, | |||||
manuals: { | |||||
title: "製品マニュアル", | |||||
description: "製品取扱説明書とインストールガイドをダウンロード" | |||||
}, | |||||
drivers: { | |||||
title: "ドライバーダウンロード", | |||||
description: "最新の製品ドライバーとソフトウェアを取得" | |||||
}, | |||||
chat: { | |||||
title: "ライブチャット", | |||||
description: "技術専門家とリアルタイムでチャット" | |||||
}, | |||||
backToSupport: "技術サポートに戻る" | |||||
}, | }, | ||||
about: { | about: { | ||||
title: "当社について - Hanye", | title: "当社について - Hanye", |
category: "分类", | category: "分类", | ||||
clearSearch: "清除搜索", | clearSearch: "清除搜索", | ||||
noResults: "没有找到相关问题", | noResults: "没有找到相关问题", | ||||
processError: "处理FAQ数据时出错", | |||||
}, | |||||
support: { | |||||
title: "技术支持", | |||||
subtitle: "我们提供全方位的技术支持服务,帮助您解决产品使用中的各种问题", | |||||
description: "获取专业的技术支持服务,包括常见问题、产品手册、驱动下载等", | |||||
keywords: "Hanye, 技术支持, 客服, FAQ, 产品手册, 驱动下载", | |||||
viewMore: "查看更多", | |||||
contactUs: "联系我们", | |||||
viewDocs: "查看文档", | |||||
download: "下载", | |||||
startChat: "开始对话", | |||||
comingSoon: "即将上线", | |||||
faq: { | |||||
title: "常见问题", | |||||
description: "查看最常见的产品问题和解答" | |||||
}, | |||||
contact: { | |||||
title: "联系我们", | |||||
description: "直接联系我们的技术支持团队" | |||||
}, | |||||
docs: { | |||||
title: "技术文档", | |||||
description: "查看详细的技术文档和使用指南" | |||||
}, | |||||
manuals: { | |||||
title: "产品手册", | |||||
description: "下载产品说明书和安装指南" | |||||
}, | |||||
drivers: { | |||||
title: "驱动下载", | |||||
description: "获取最新的产品驱动程序" | |||||
}, | |||||
chat: { | |||||
title: "在线客服", | |||||
description: "与我们的技术专家实时对话" | |||||
}, | |||||
backToSupport: "返回技术支持" | |||||
}, | }, | ||||
about: { | about: { | ||||
title: "关于我们", | title: "关于我们", |
<!-- 关于我们 (公司简介) --> | <!-- 关于我们 (公司简介) --> | ||||
<div | <div | ||||
id="company-profile" | id="company-profile" | ||||
class="section-block w-full bg-gradient-to-b from-zinc-900 via-zinc-800/80 to-zinc-900 py-40" | |||||
class="section-block w-full bg-gradient-to-b from-zinc-900 via-zinc-800/80 to-zinc-900 py-40 relative overflow-hidden" | |||||
ref="companyProfileRef" | ref="companyProfileRef" | ||||
> | > | ||||
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8"> | |||||
<!-- 商务风格背景图 --> | |||||
<div class="absolute inset-0 bg-business-overlay"></div> | |||||
<div class="absolute inset-0 bg-gradient-to-b from-black/70 via-black/50 to-black/70"></div> | |||||
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10"> | |||||
<div class="text-center mb-32 animate-fade-in"> | <div class="text-center mb-32 animate-fade-in"> | ||||
<h2 | <h2 | ||||
class="section-title text-[#35F1FF] text-sm font-bold mb-4 tracking-[0.2em] animate-slide-in-left" | class="section-title text-[#35F1FF] text-sm font-bold mb-4 tracking-[0.2em] animate-slide-in-left" | ||||
// 使用 Intersection Observer 优化性能和准确性 | // 使用 Intersection Observer 优化性能和准确性 | ||||
const observerOptions = { | const observerOptions = { | ||||
root: null, // 视口 | root: null, // 视口 | ||||
rootMargin: '0px', | |||||
threshold: 0.4 // 元素 40% 可见时触发 | |||||
rootMargin: '-20% 0px -20% 0px', // 提前触发动画 | |||||
threshold: [0, 0.1, 0.2, 0.3] // 多个阈值,更灵敏 | |||||
}; | }; | ||||
const observerCallback = (entries: IntersectionObserverEntry[]) => { | const observerCallback = (entries: IntersectionObserverEntry[]) => { | ||||
entries.forEach(entry => { | entries.forEach(entry => { | ||||
if (entry.isIntersecting) { | |||||
if (entry.isIntersecting && entry.intersectionRatio > 0.1) { | |||||
currentSection.value = entry.target.id; | currentSection.value = entry.target.id; | ||||
entry.target.classList.add('animate-in'); | entry.target.classList.add('animate-in'); | ||||
} | } | ||||
contactRef.value | contactRef.value | ||||
]; | ]; | ||||
sectionElements.forEach(el => { | sectionElements.forEach(el => { | ||||
if (el) observer.observe(el); | |||||
if (el) { | |||||
observer.observe(el); | |||||
// 检查是否已经在视口中,如果是则立即显示 | |||||
const rect = el.getBoundingClientRect(); | |||||
const windowHeight = window.innerHeight; | |||||
if (rect.top < windowHeight * 0.8 && rect.bottom > 0) { | |||||
el.classList.add('animate-in'); | |||||
} | |||||
} | |||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
// 处理滚动 (保留用于横幅视差效果) | |||||
// 处理滚动 (保留用于横幅视差效果和备用动画触发) | |||||
const handleLegacyScroll = () => { | const handleLegacyScroll = () => { | ||||
scrollY.value = window.scrollY; | scrollY.value = window.scrollY; | ||||
// 备用动画触发机制 | |||||
const sections = [ | |||||
companyProfileRef.value, | |||||
businessContentRef.value, | |||||
philosophyRef.value, | |||||
companyInfoRef.value, | |||||
contactRef.value | |||||
]; | |||||
sections.forEach(section => { | |||||
if (section && !section.classList.contains('animate-in')) { | |||||
const rect = section.getBoundingClientRect(); | |||||
const windowHeight = window.innerHeight; | |||||
// 当元素进入视口的80%时触发动画 | |||||
if (rect.top < windowHeight * 0.8 && rect.bottom > 0) { | |||||
section.classList.add('animate-in'); | |||||
} | |||||
} | |||||
}); | |||||
}; | }; | ||||
// SEO优化 | // SEO优化 | ||||
</script> | </script> | ||||
<style scoped> | <style scoped> | ||||
/* 商务风格背景图样式 */ | |||||
.bg-business-overlay { | |||||
background-image: url('/assets/images/about.webp'); | |||||
background-size: cover; | |||||
background-position: center; | |||||
background-repeat: no-repeat; | |||||
background-attachment: fixed; | |||||
opacity: 0.3; | |||||
transition: all 0.5s ease; | |||||
} | |||||
@media (max-width: 768px) { | |||||
.bg-business-overlay { | |||||
background-attachment: scroll; /* 移动端不支持 fixed */ | |||||
opacity: 0.2; | |||||
} | |||||
} | |||||
/* 悬停效果 */ | |||||
#company-profile:hover .bg-business-overlay { | |||||
opacity: 0.4; | |||||
transform: scale(1.02); | |||||
} | |||||
#company-profile:hover { | |||||
background: linear-gradient(to bottom, rgba(53, 241, 255, 0.08), rgba(53, 241, 255, 0.04)); | |||||
} | |||||
/* 基础动画类 */ | /* 基础动画类 */ | ||||
.section-block { | .section-block { | ||||
opacity: 0; | opacity: 0; |
<template> | |||||
<div> | |||||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||||
<ErrorBoundary :error="error"> | |||||
<div v-if="isLoading" class="flex justify-center py-12"> | |||||
<div | |||||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
></div> | |||||
</div> | |||||
<div v-else> | |||||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-12"> | |||||
<div class="max-w-screen-2xl mx-auto"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/`" | |||||
class="justify-start text-white/60 text-sm md:text-base font-normal hover:text-white transition-colors duration-300" | |||||
>{{ t("common.home") }}</nuxt-link | |||||
> | |||||
<span class="text-white/60 text-sm md:text-base font-normal px-2"> | |||||
/ | |||||
</span> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/faq`" | |||||
class="text-white text-sm md:text-base font-normal" | |||||
>{{ t("faq.title") }}</nuxt-link | |||||
> | |||||
</div> | |||||
</div> | |||||
<div | |||||
class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:mb-20 xl:px-8 lg:px-6 md:px-4 px-4" | |||||
> | |||||
<div class="max-w-screen-2xl mx-auto"> | |||||
<div class="w-full grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-4"> | |||||
<!-- 左侧分类导航 --> | |||||
<div | |||||
class="col-span-1 md:col-span-3 flex flex-col gap-4 sm:gap-6 md:gap-8 lg:gap-10 xl:gap-12 2xl:gap-16 mb-4 sm:mb-6 md:mb-8 lg:mb-10 xl:mb-12 2xl:mb-16 pr-4" | |||||
> | |||||
<div | |||||
class="flex flex-col gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6" | |||||
> | |||||
<div class="flex justify-between items-center"> | |||||
<div | |||||
class="text-white text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl font-medium" | |||||
> | |||||
{{ t("faq.category") }} | |||||
</div> | |||||
</div> | |||||
<div | |||||
class="flex flex-row md:flex-col gap-1.5 sm:gap-2 md:gap-2.5 lg:gap-3 xl:gap-4 w-full md:w-fit overflow-x-auto md:overflow-x-visible pb-2 md:pb-0 whitespace-nowrap md:whitespace-normal" | |||||
> | |||||
<div | |||||
v-for="category in categoriesList" | |||||
:key="category" | |||||
@click="handleCategoryFilter(category)" | |||||
class="select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95 whitespace-nowrap" | |||||
:class="{ | |||||
'font-bold bg-cyan-400 text-zinc-900 border-0 shadow-lg scale-105 transition-all duration-300': | |||||
selectedCategory === category, | |||||
'hover:bg-zinc-800/50': selectedCategory !== category, | |||||
}" | |||||
> | |||||
{{ category }} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 右侧FAQ列表 --> | |||||
<div class="col-span-1 md:col-span-9"> | |||||
<!-- 搜索框 --> | |||||
<div class="mb-8 relative"> | |||||
<input | |||||
v-model="searchTerm" | |||||
ref="searchInputRef" | |||||
type="search" | |||||
:placeholder="t('faq.searchPlaceholder')" | |||||
class="block w-full appearance-none rounded-lg border border-gray-600 bg-zinc-800/70 px-4 py-3 text-base text-gray-100 placeholder-gray-400 shadow-inner transition duration-200 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 focus:ring-offset-zinc-900" | |||||
/> | |||||
<button | |||||
v-if="searchTerm" | |||||
@click="clearSearch" | |||||
class="absolute right-2 top-1/2 -translate-y-1/2 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('faq.clearSearch')" | |||||
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> | |||||
</div> | |||||
<div class="flex flex-col gap-8"> | |||||
<div | |||||
v-for="faq in filteredFaqs" | |||||
:key="generateFaqKey(faq)" | |||||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | |||||
> | |||||
<div | |||||
class="flex items-center justify-between cursor-pointer" | |||||
@click="toggleFaq(faq)" | |||||
> | |||||
<div class="text-white text-xl font-medium"> | |||||
<template | |||||
v-for="(part, i) in highlightKeyword(faq.title)" | |||||
:key="i" | |||||
> | |||||
<span v-if="typeof part === 'string'">{{ | |||||
part | |||||
}}</span> | |||||
<component v-else :is="part"></component> | |||||
</template> | |||||
</div> | |||||
<div | |||||
class="text-white text-2xl transition-transform duration-300" | |||||
:class="{ 'rotate-180': isFaqExpanded(faq) }" | |||||
> | |||||
▼ | |||||
</div> | |||||
</div> | |||||
<div v-if="isFaqExpanded(faq)" class="mt-4"> | |||||
<ContentRenderer | |||||
class="prose prose-invert w-full max-w-none faq-content" | |||||
:value="{ body: faq.content }" | |||||
v-highlight="searchTerm.trim()" | |||||
/> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="filteredFaqs.length === 0" | |||||
class="text-center text-gray-400 py-8" | |||||
> | |||||
{{ t("faq.noResults") }} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</ErrorBoundary> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* FAQ页面 | |||||
* 展示常见问题及其答案 | |||||
*/ | |||||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||||
import { queryCollection } from "#imports"; | |||||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||||
const { t, locale } = useI18n(); | |||||
// FAQ数据 | |||||
interface FAQ { | |||||
category: string; | |||||
title: string; | |||||
content: any; // 修改为 any 类型,因为可能是对象或字符串 | |||||
sort: number; | |||||
id?: string; | |||||
} | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "" : `/${locale.value}`; | |||||
}); | |||||
// 从content目录读取FAQ数据 | |||||
const faqs = ref<FAQ[]>([]); | |||||
const categoriesList = ref<string[]>([]); | |||||
// 选中的分类 | |||||
const selectedCategory = ref(""); | |||||
// 使用 queryCollection 加载FAQ数据 | |||||
const { data: faqData } = await useAsyncData( | |||||
"faqs", | |||||
async () => { | |||||
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); | |||||
return []; | |||||
} | |||||
// 转换数据格式 | |||||
const faqItems = content.map((item: any) => { | |||||
return { | |||||
category: item.meta?.category || "", | |||||
title: item.title || "", | |||||
content: item.body || "", | |||||
sort: item.meta?.sort || 0, | |||||
}; | |||||
}); | |||||
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], | |||||
} | |||||
); | |||||
// 处理FAQ数据变化 | |||||
watchEffect(() => { | |||||
if (faqData.value) { | |||||
isLoading.value = true; | |||||
try { | |||||
// 设置分类列表和默认选中的分类 | |||||
const allOption: string = | |||||
locale.value === "en" | |||||
? "All" | |||||
: locale.value === "zh" | |||||
? "全部" | |||||
: "すべて"; | |||||
// 从FAQ数据中提取所有不同的分类 | |||||
const uniqueCategories = [ | |||||
...new Set(faqData.value.map((faq: FAQ) => faq.category)), | |||||
] | |||||
.filter((category) => category) | |||||
.sort(); // 过滤掉空分类并排序 | |||||
// 设置分类列表和默认选中的分类 | |||||
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")); | |||||
} finally { | |||||
isLoading.value = false; | |||||
} | |||||
} | |||||
}); | |||||
// 展开的FAQ标识列表 | |||||
const expandedFaqKeys = ref<Set<string>>(new Set()); | |||||
// 搜索关键词 | |||||
const searchTerm = ref(""); | |||||
const searchInputRef = ref<HTMLInputElement | null>(null); | |||||
// 过滤后的FAQ列表 | |||||
const filteredFaqs = computed(() => { | |||||
if (!faqData.value) { | |||||
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) => { | |||||
const title = String(faq.title || "").toLowerCase(); | |||||
// 处理内容可能是对象或字符串的情况 | |||||
let contentText = ""; | |||||
if (faq.content) { | |||||
if (typeof faq.content === "string") { | |||||
contentText = faq.content.toLowerCase(); | |||||
} else if (typeof faq.content === "object") { | |||||
// 如果是对象,尝试提取内容 | |||||
const contentObj = faq.content; | |||||
if (contentObj.children && Array.isArray(contentObj.children)) { | |||||
// 如果有children数组,遍历提取文本 | |||||
contentText = JSON.stringify(contentObj); | |||||
} else { | |||||
// 其他情况,尝试转换整个对象为字符串 | |||||
contentText = JSON.stringify(contentObj).toLowerCase(); | |||||
} | |||||
} | |||||
} | |||||
return title.includes(keyword) || contentText.includes(keyword); | |||||
}); | |||||
} | |||||
return result; | |||||
}); | |||||
/** | |||||
* 高亮显示匹配的关键字 | |||||
* @param text 原始文本 | |||||
* @returns 高亮后的VNode数组 | |||||
*/ | |||||
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 = textStr.split(reg); | |||||
const matches = textStr.match(reg); | |||||
if (!matches) return [textStr]; | |||||
// 组装高亮 | |||||
const result: (string | any)[] = []; | |||||
parts.forEach((part, i) => { | |||||
result.push(part); | |||||
if (i < matches.length) { | |||||
result.push( | |||||
h( | |||||
"span", | |||||
{ class: "text-blue-400 bg-blue-400/10 font-bold" }, | |||||
matches[i] | |||||
) | |||||
); | |||||
} | |||||
}); | |||||
return result; | |||||
} | |||||
/** | |||||
* 生成FAQ的唯一标识 | |||||
* @param faq FAQ对象 | |||||
* @returns string 唯一标识 | |||||
*/ | |||||
function generateFaqKey(faq: FAQ): string { | |||||
return `${faq.category}-${faq.title}`; | |||||
} | |||||
/** | |||||
* 切换FAQ展开状态 | |||||
* @param faq FAQ对象 | |||||
* @returns void | |||||
*/ | |||||
function toggleFaq(faq: FAQ): void { | |||||
if (!faq) return; | |||||
const faqKey = generateFaqKey(faq); | |||||
// 如果当前FAQ已展开,则关闭它 | |||||
if (expandedFaqKeys.value.has(faqKey)) { | |||||
expandedFaqKeys.value.delete(faqKey); | |||||
} else { | |||||
// 否则展开它(不会关闭其他FAQ) | |||||
expandedFaqKeys.value.add(faqKey); | |||||
} | |||||
} | |||||
/** | |||||
* 检查FAQ是否处于展开状态 | |||||
* @param faq FAQ对象 | |||||
* @returns boolean | |||||
*/ | |||||
function isFaqExpanded(faq: FAQ): boolean { | |||||
if (!faq) return false; | |||||
return expandedFaqKeys.value.has(generateFaqKey(faq)); | |||||
} | |||||
/** | |||||
* 处理分类筛选 | |||||
*/ | |||||
function handleCategoryFilter(category: string) { | |||||
selectedCategory.value = category; | |||||
} | |||||
// 自动展开匹配项 | |||||
watch( | |||||
[filteredFaqs, searchTerm], | |||||
([faqs, keyword]: [FAQ[], string]) => { | |||||
if (!faqs?.length) return; | |||||
if (keyword.trim()) { | |||||
// 搜索时展开所有匹配项 | |||||
expandedFaqKeys.value = new Set(faqs.map((faq) => generateFaqKey(faq))); | |||||
} else { | |||||
// 清除搜索时关闭所有展开项 | |||||
expandedFaqKeys.value.clear(); | |||||
} | |||||
}, | |||||
{ deep: true } | |||||
); | |||||
function clearSearch() { | |||||
searchTerm.value = ""; | |||||
// 让输入框重新聚焦 | |||||
searchInputRef.value?.focus(); | |||||
} | |||||
// SEO优化 | |||||
useHead({ | |||||
title: t("faq.title") + " - Hanye", | |||||
meta: [ | |||||
{ | |||||
name: "description", | |||||
content: t("faq.description"), | |||||
}, | |||||
{ | |||||
name: "keywords", | |||||
content: t("faq.keywords"), | |||||
}, | |||||
], | |||||
}); | |||||
// 添加自定义指令,用于在不破坏HTML结构的情况下高亮内容 | |||||
const vHighlight = { | |||||
mounted(el: HTMLElement, binding: { value: string }) { | |||||
highlight(el, binding.value); | |||||
}, | |||||
updated(el: HTMLElement, binding: { value: string }) { | |||||
highlight(el, binding.value); | |||||
}, | |||||
}; | |||||
// 注册指令 | |||||
const app = useNuxtApp(); | |||||
app.vueApp.directive("highlight", vHighlight); | |||||
// 高亮处理函数 | |||||
function highlight(el: HTMLElement, keyword: string) { | |||||
if (!keyword || typeof keyword !== "string" || !keyword.trim()) { | |||||
return; | |||||
} | |||||
// 递归处理节点 | |||||
function highlightNode(node: Node): boolean { | |||||
if (node.nodeType === Node.TEXT_NODE) { | |||||
// 文本节点,可以高亮 | |||||
const text = node.textContent || ""; | |||||
const lowerText = text.toLowerCase(); | |||||
const lowerKeyword = keyword.toLowerCase(); | |||||
if (lowerText.includes(lowerKeyword)) { | |||||
const fragment = document.createDocumentFragment(); | |||||
let lastIndex = 0; | |||||
// 查找所有匹配项 | |||||
const regex = new RegExp(escapeRegExp(keyword), "gi"); | |||||
let match; | |||||
while ((match = regex.exec(text)) !== null) { | |||||
// 添加匹配前的文本 | |||||
if (match.index > lastIndex) { | |||||
fragment.appendChild( | |||||
document.createTextNode(text.substring(lastIndex, match.index)) | |||||
); | |||||
} | |||||
// 创建高亮的span | |||||
const highlightSpan = document.createElement("span"); | |||||
highlightSpan.className = "text-blue-400 bg-blue-400/10 font-bold"; | |||||
highlightSpan.textContent = match[0]; | |||||
fragment.appendChild(highlightSpan); | |||||
lastIndex = regex.lastIndex; | |||||
} | |||||
// 添加匹配后的剩余文本 | |||||
if (lastIndex < text.length) { | |||||
fragment.appendChild( | |||||
document.createTextNode(text.substring(lastIndex)) | |||||
); | |||||
} | |||||
// 替换原节点 | |||||
if (node.parentNode) { | |||||
node.parentNode.replaceChild(fragment, node); | |||||
} | |||||
return true; | |||||
} | |||||
} else if (node.nodeType === Node.ELEMENT_NODE) { | |||||
// 元素节点,递归处理子节点 | |||||
// 避免处理这些标签内的内容 | |||||
const element = node as HTMLElement; | |||||
if ( | |||||
["SCRIPT", "STYLE", "TEXTAREA", "INPUT", "SELECT", "OPTION"].includes( | |||||
element.tagName | |||||
) | |||||
) { | |||||
return false; | |||||
} | |||||
// 创建节点的副本避免在迭代过程中修改节点列表 | |||||
const childNodes = Array.from(node.childNodes); | |||||
childNodes.forEach((child) => highlightNode(child)); | |||||
} | |||||
return false; | |||||
} | |||||
highlightNode(el); | |||||
} | |||||
// 转义正则表达式中的特殊字符 | |||||
function escapeRegExp(string: string): string { | |||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$0"); | |||||
} | |||||
</script> | |||||
<style scoped> | |||||
/* 添加过渡动画 */ | |||||
.fade-enter-active, | |||||
.fade-leave-active { | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.fade-enter-from, | |||||
.fade-leave-to { | |||||
opacity: 0; | |||||
transform: translateY(20px); | |||||
} | |||||
/* 分类悬停效果 */ | |||||
.active\:scale-95:active { | |||||
transform: scale(0.95); | |||||
} | |||||
/* 优化横向滚动 */ | |||||
.overflow-x-auto { | |||||
-webkit-overflow-scrolling: touch; | |||||
scrollbar-width: none; /* Firefox */ | |||||
-ms-overflow-style: none; /* IE and Edge */ | |||||
} | |||||
.overflow-x-auto::-webkit-scrollbar { | |||||
display: none; /* Chrome, Safari, Opera */ | |||||
} | |||||
/* 清除按钮动画 */ | |||||
button { | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
button:hover { | |||||
transform: translateY(-2px); | |||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), | |||||
0 2px 4px -1px rgba(0, 0, 0, 0.06); | |||||
} | |||||
button:active { | |||||
transform: translateY(0); | |||||
} | |||||
/* FAQ项目悬停效果 */ | |||||
.bg-zinc-900 { | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-4px); | |||||
} | |||||
/* 响应式调整 */ | |||||
@media (max-width: 768px) { | |||||
button:hover { | |||||
transform: translateY(-1px); | |||||
} | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-2px); | |||||
} | |||||
} | |||||
@media (min-width: 1024px) { | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-4px); | |||||
} | |||||
} | |||||
/* 高亮内容样式 */ | |||||
.faq-content { | |||||
white-space: pre-line; | |||||
line-height: 1.6; | |||||
color: #e2e8f0; | |||||
} | |||||
.faq-content :deep(ul), | |||||
.faq-content :deep(ol) { | |||||
padding-left: 1.5rem; | |||||
margin: 1rem 0; | |||||
} | |||||
.faq-content :deep(li) { | |||||
margin: 0.5rem 0; | |||||
} | |||||
.faq-content :deep(p) { | |||||
margin: 0.75rem 0; | |||||
} | |||||
.faq-content :deep(h1), | |||||
.faq-content :deep(h2), | |||||
.faq-content :deep(h3), | |||||
.faq-content :deep(h4), | |||||
.faq-content :deep(h5), | |||||
.faq-content :deep(h6) { | |||||
margin: 1.5rem 0 0.75rem; | |||||
font-weight: 600; | |||||
color: #f8fafc; | |||||
} | |||||
</style> |
<nuxt-link | <nuxt-link | ||||
v-for="(recommend, index) in recommendList" | v-for="(recommend, index) in recommendList" | ||||
:key="index" | :key="index" | ||||
:to="`${homepagePath}products/${recommend.name}`" | |||||
:to="`${homepagePath}/products/${recommend.name}`" | |||||
class="w-full h-full" | class="w-full h-full" | ||||
> | > | ||||
<div | <div |
<template> | |||||
<div class="min-h-screen bg-stone-950"> | |||||
<div class="w-full h-[45px] sm:h-[55px] md:h-[65px] lg:h-[72px]"></div> | |||||
<!-- 面包屑导航 --> | |||||
<div class="max-w-screen-2xl mx-auto px-4 pt-8"> | |||||
<nav class="flex items-center space-x-2 text-sm text-zinc-400"> | |||||
<NuxtLink :to="homepagePath" class="hover:text-white transition">{{ | |||||
t("common.breadcrumb.home") | |||||
}}</NuxtLink> | |||||
<span class="text-zinc-600">/</span> | |||||
<span class="text-white">{{ t("common.breadcrumb.support") }}</span> | |||||
</nav> | |||||
</div> | |||||
<!-- 主要内容 --> | |||||
<div class="max-w-screen-2xl mx-auto px-4 py-8"> | |||||
<!-- 页面标题 --> | |||||
<div class="mb-8"> | |||||
<h1 class="text-4xl font-bold text-white mb-4"> | |||||
{{ t("common.support.title") }} | |||||
</h1> | |||||
<p class="text-zinc-400">{{ t("common.support.description") }}</p> | |||||
</div> | |||||
<!-- 内容区域 --> | |||||
<div class="bg-stone-900 rounded-lg p-8 shadow-lg"> | |||||
<div class="prose prose-invert max-w-none"> | |||||
<ContentRenderer v-if="page" :value="page" /> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* サポートページ | |||||
* ウェブサイトのサポート情報を表示 | |||||
*/ | |||||
import { useI18n } from 'vue-i18n' | |||||
import { computed } from 'vue' | |||||
const { t, locale } = useI18n() | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "/" : `/${locale.value}` | |||||
}) | |||||
// 计算内容路径 | |||||
const contentPath = computed(() => { | |||||
return `/${locale.value}/support` | |||||
}) | |||||
const { data: page } = await useAsyncData(contentPath.value, () => { | |||||
return queryCollection("content").path(contentPath.value).first() | |||||
}) | |||||
// 设置页面元数据 | |||||
useHead({ | |||||
title: t("common.meta.support.title"), | |||||
meta: [ | |||||
{ | |||||
name: "description", | |||||
content: t("common.meta.support.description"), | |||||
}, | |||||
], | |||||
}) | |||||
</script> |
<template> | |||||
<div class="min-h-screen bg-gray-900"> | |||||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||||
<ErrorBoundary :error="error"> | |||||
<div v-if="isLoading" class="flex justify-center py-16"> | |||||
<div class="animate-spin h-8 w-8 border-2 border-blue-400 border-t-transparent rounded-full"></div> | |||||
</div> | |||||
<div v-else> | |||||
<!-- 面包屑导航 --> | |||||
<div class="bg-gray-800 border-b border-gray-700"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> | |||||
<nav class="flex items-center space-x-2 text-sm text-gray-400"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/`" | |||||
class="hover:text-blue-400 transition-colors duration-200" | |||||
> | |||||
{{ t("common.home") }} | |||||
</nuxt-link> | |||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |||||
</svg> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/support`" | |||||
class="hover:text-blue-400 transition-colors duration-200" | |||||
> | |||||
{{ t("support.title") }} | |||||
</nuxt-link> | |||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |||||
</svg> | |||||
<span class="text-gray-200 font-medium">{{ t("support.faq.title") }}</span> | |||||
</nav> | |||||
</div> | |||||
</div> | |||||
<!-- 页面头部 --> | |||||
<div class="bg-gray-800"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | |||||
<div class="text-center"> | |||||
<div class="flex items-center justify-center mb-6"> | |||||
<div class="w-12 h-12 bg-blue-500/20 text-blue-400 rounded-lg flex items-center justify-center mr-4"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<h1 class="text-3xl md:text-4xl font-bold text-white"> | |||||
{{ t("support.faq.title") }} | |||||
</h1> | |||||
</div> | |||||
<p class="text-xl text-gray-300 max-w-3xl mx-auto"> | |||||
{{ t("support.faq.description") }} | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- FAQ内容区域 --> | |||||
<div class="bg-gray-900"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | |||||
<div class="bg-gray-800 rounded-lg shadow-lg border border-gray-700 overflow-hidden"> | |||||
<FaqContent /> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 底部操作区域 --> | |||||
<div class="bg-gray-800 border-t border-gray-700"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |||||
<div class="flex flex-col sm:flex-row items-center justify-between"> | |||||
<div class="mb-4 sm:mb-0"> | |||||
<p class="text-gray-400 text-sm"> | |||||
没有找到您需要的答案?我们的技术支持团队随时为您提供帮助。 | |||||
</p> | |||||
</div> | |||||
<div class="flex flex-col sm:flex-row gap-3"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/support`" | |||||
class="inline-flex items-center px-4 py-2 border border-gray-600 text-sm font-medium rounded-md text-gray-300 bg-gray-700 hover:bg-gray-600 transition-colors duration-200" | |||||
> | |||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> | |||||
</svg> | |||||
{{ t("support.backToSupport") }} | |||||
</nuxt-link> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/contact`" | |||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors duration-200" | |||||
> | |||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |||||
</svg> | |||||
联系技术支持 | |||||
</nuxt-link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</ErrorBoundary> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* FAQ详情页面 | |||||
* 专业的常见问题解答页面 | |||||
*/ | |||||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||||
const { error, isLoading } = useErrorHandler(); | |||||
const { t, locale } = useI18n(); | |||||
// 计算首页路径 | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "" : `/${locale.value}`; | |||||
}); | |||||
// SEO优化 | |||||
useHead({ | |||||
title: t("support.faq.title") + " - " + t("support.title") + " - Hanye", | |||||
meta: [ | |||||
{ | |||||
name: "description", | |||||
content: t("faq.description"), | |||||
}, | |||||
{ | |||||
name: "keywords", | |||||
content: t("faq.keywords"), | |||||
}, | |||||
], | |||||
}); | |||||
</script> | |||||
<style scoped> | |||||
/* 过渡效果 */ | |||||
.transition-colors { | |||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.duration-200 { | |||||
transition-duration: 200ms; | |||||
} | |||||
/* 悬停效果 */ | |||||
.hover\:bg-blue-700:hover { | |||||
background-color: #1d4ed8; | |||||
} | |||||
.hover\:bg-gray-600:hover { | |||||
background-color: #4b5563; | |||||
} | |||||
.hover\:text-blue-400:hover { | |||||
color: #60a5fa; | |||||
} | |||||
/* 响应式优化 */ | |||||
@media (max-width: 768px) { | |||||
.flex-col { | |||||
align-items: stretch; | |||||
} | |||||
.flex-col > * { | |||||
width: 100%; | |||||
justify-content: center; | |||||
} | |||||
} | |||||
</style> |
<template> | |||||
<div class="min-h-screen bg-gray-900"> | |||||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||||
<ErrorBoundary :error="error"> | |||||
<div v-if="isLoading" class="flex justify-center py-16"> | |||||
<div class="animate-spin h-8 w-8 border-2 border-blue-400 border-t-transparent rounded-full"></div> | |||||
</div> | |||||
<div v-else> | |||||
<!-- 面包屑导航 --> | |||||
<div class="bg-gray-800 border-b border-gray-700"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> | |||||
<nav class="flex items-center space-x-2 text-sm text-gray-400"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/`" | |||||
class="hover:text-blue-400 transition-colors duration-200" | |||||
> | |||||
{{ t("common.home") }} | |||||
</nuxt-link> | |||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |||||
</svg> | |||||
<span class="text-gray-200 font-medium">{{ t("support.title") }}</span> | |||||
</nav> | |||||
</div> | |||||
</div> | |||||
<!-- 页面头部 --> | |||||
<div class="bg-gray-800"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | |||||
<div class="text-center"> | |||||
<h1 class="text-4xl font-bold text-white mb-4"> | |||||
{{ t("support.title") }} | |||||
</h1> | |||||
<p class="text-xl text-gray-300 max-w-3xl mx-auto"> | |||||
{{ t("support.subtitle") }} | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 主要服务区域 --> | |||||
<div class="bg-gray-900"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | |||||
<!-- 主要服务 --> | |||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-16"> | |||||
<!-- FAQ卡片 --> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/support/faq`" | |||||
class="group bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8 hover:shadow-xl hover:border-blue-500/50 transition-all duration-200" | |||||
> | |||||
<div class="flex items-start"> | |||||
<div class="flex-shrink-0"> | |||||
<div class="w-12 h-12 bg-blue-500/20 text-blue-400 rounded-lg flex items-center justify-center"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
</div> | |||||
<div class="ml-6 flex-1"> | |||||
<h3 class="text-xl font-semibold text-white mb-3 group-hover:text-blue-400 transition-colors"> | |||||
{{ t("support.faq.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 mb-4 leading-relaxed"> | |||||
{{ t("support.faq.description") }} | |||||
</p> | |||||
<div class="flex items-center text-blue-400 font-medium text-sm"> | |||||
{{ t("support.viewMore") }} | |||||
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |||||
</svg> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</nuxt-link> | |||||
<!-- 联系我们卡片 --> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/contact`" | |||||
class="group bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8 hover:shadow-xl hover:border-green-500/50 transition-all duration-200" | |||||
> | |||||
<div class="flex items-start"> | |||||
<div class="flex-shrink-0"> | |||||
<div class="w-12 h-12 bg-green-500/20 text-green-400 rounded-lg flex items-center justify-center"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |||||
</svg> | |||||
</div> | |||||
</div> | |||||
<div class="ml-6 flex-1"> | |||||
<h3 class="text-xl font-semibold text-white mb-3 group-hover:text-green-400 transition-colors"> | |||||
{{ t("support.contact.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 mb-4 leading-relaxed"> | |||||
{{ t("support.contact.description") }} | |||||
</p> | |||||
<div class="flex items-center text-green-400 font-medium text-sm"> | |||||
{{ t("support.contactUs") }} | |||||
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |||||
</svg> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</nuxt-link> | |||||
</div> | |||||
<!-- 其他服务 --> | |||||
<div class="border-t border-gray-700 pt-16"> | |||||
<h2 class="text-2xl font-bold text-white text-center mb-12">其他支持服务</h2> | |||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> | |||||
<!-- 技术文档 --> | |||||
<div | |||||
class="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6 text-center cursor-pointer hover:shadow-xl hover:border-purple-500/50 transition-all duration-200" | |||||
@click="showDocsModal = true" | |||||
> | |||||
<div class="w-12 h-12 bg-purple-500/20 text-purple-400 rounded-lg flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2"> | |||||
{{ t("support.docs.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 text-sm mb-4"> | |||||
{{ t("support.docs.description") }} | |||||
</p> | |||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-700 text-gray-300"> | |||||
{{ t("support.comingSoon") }} | |||||
</span> | |||||
</div> | |||||
<!-- 产品手册 --> | |||||
<div | |||||
class="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6 text-center cursor-pointer hover:shadow-xl hover:border-orange-500/50 transition-all duration-200" | |||||
@click="showManualsModal = true" | |||||
> | |||||
<div class="w-12 h-12 bg-orange-500/20 text-orange-400 rounded-lg flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2"> | |||||
{{ t("support.manuals.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 text-sm mb-4"> | |||||
{{ t("support.manuals.description") }} | |||||
</p> | |||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-700 text-gray-300"> | |||||
{{ t("support.comingSoon") }} | |||||
</span> | |||||
</div> | |||||
<!-- 驱动下载 --> | |||||
<div | |||||
class="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6 text-center cursor-pointer hover:shadow-xl hover:border-cyan-500/50 transition-all duration-200" | |||||
@click="showDriversModal = true" | |||||
> | |||||
<div class="w-12 h-12 bg-cyan-500/20 text-cyan-400 rounded-lg flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2"> | |||||
{{ t("support.drivers.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 text-sm mb-4"> | |||||
{{ t("support.drivers.description") }} | |||||
</p> | |||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-700 text-gray-300"> | |||||
{{ t("support.comingSoon") }} | |||||
</span> | |||||
</div> | |||||
<!-- 在线客服 --> | |||||
<div | |||||
class="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6 text-center cursor-pointer hover:shadow-xl hover:border-red-500/50 transition-all duration-200" | |||||
@click="showChatModal = true" | |||||
> | |||||
<div class="w-12 h-12 bg-red-500/20 text-red-400 rounded-lg flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2"> | |||||
{{ t("support.chat.title") }} | |||||
</h3> | |||||
<p class="text-gray-400 text-sm mb-4"> | |||||
{{ t("support.chat.description") }} | |||||
</p> | |||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-700 text-gray-300"> | |||||
{{ t("support.comingSoon") }} | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 联系信息区域 --> | |||||
<div class="bg-gray-800 border-t border-gray-700"> | |||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | |||||
<div class="text-center"> | |||||
<h2 class="text-2xl font-bold text-white mb-4">需要进一步帮助?</h2> | |||||
<p class="text-gray-300 mb-8 max-w-2xl mx-auto"> | |||||
我们的专业技术支持团队随时为您提供帮助,确保您获得最佳的产品体验。 | |||||
</p> | |||||
<div class="flex flex-col sm:flex-row gap-4 justify-center"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/contact`" | |||||
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors duration-200" | |||||
> | |||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |||||
</svg> | |||||
联系技术支持 | |||||
</nuxt-link> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/support/faq`" | |||||
class="inline-flex items-center px-6 py-3 border border-gray-600 text-base font-medium rounded-md text-gray-300 bg-gray-700 hover:bg-gray-600 transition-colors duration-200" | |||||
> | |||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
浏览常见问题 | |||||
</nuxt-link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 模态框 --> | |||||
<div | |||||
v-if="showDocsModal" | |||||
class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4" | |||||
@click="showDocsModal = false" | |||||
> | |||||
<div | |||||
class="bg-gray-800 rounded-lg max-w-md w-full shadow-2xl border border-gray-700" | |||||
@click.stop | |||||
> | |||||
<div class="flex items-center justify-between p-6 border-b border-gray-700"> | |||||
<h2 class="text-xl font-semibold text-white">{{ t("support.docs.title") }}</h2> | |||||
<button | |||||
@click="showDocsModal = false" | |||||
class="text-gray-400 hover:text-gray-200 transition-colors" | |||||
> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
</div> | |||||
<div class="p-6 text-center"> | |||||
<div class="w-16 h-16 bg-purple-500/20 text-purple-400 rounded-full flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2">{{ t("support.comingSoon") }}</h3> | |||||
<p class="text-gray-400 text-sm">我们正在为您准备详细的技术文档,敬请期待</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="showManualsModal" | |||||
class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4" | |||||
@click="showManualsModal = false" | |||||
> | |||||
<div | |||||
class="bg-gray-800 rounded-lg max-w-md w-full shadow-2xl border border-gray-700" | |||||
@click.stop | |||||
> | |||||
<div class="flex items-center justify-between p-6 border-b border-gray-700"> | |||||
<h2 class="text-xl font-semibold text-white">{{ t("support.manuals.title") }}</h2> | |||||
<button | |||||
@click="showManualsModal = false" | |||||
class="text-gray-400 hover:text-gray-200 transition-colors" | |||||
> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
</div> | |||||
<div class="p-6 text-center"> | |||||
<div class="w-16 h-16 bg-orange-500/20 text-orange-400 rounded-full flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2">{{ t("support.comingSoon") }}</h3> | |||||
<p class="text-gray-400 text-sm">产品手册下载功能即将上线,敬请期待</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="showDriversModal" | |||||
class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4" | |||||
@click="showDriversModal = false" | |||||
> | |||||
<div | |||||
class="bg-gray-800 rounded-lg max-w-md w-full shadow-2xl border border-gray-700" | |||||
@click.stop | |||||
> | |||||
<div class="flex items-center justify-between p-6 border-b border-gray-700"> | |||||
<h2 class="text-xl font-semibold text-white">{{ t("support.drivers.title") }}</h2> | |||||
<button | |||||
@click="showDriversModal = false" | |||||
class="text-gray-400 hover:text-gray-200 transition-colors" | |||||
> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
</div> | |||||
<div class="p-6 text-center"> | |||||
<div class="w-16 h-16 bg-cyan-500/20 text-cyan-400 rounded-full flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2">{{ t("support.comingSoon") }}</h3> | |||||
<p class="text-gray-400 text-sm">驱动程序下载区域正在准备中,敬请期待</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
v-if="showChatModal" | |||||
class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4" | |||||
@click="showChatModal = false" | |||||
> | |||||
<div | |||||
class="bg-gray-800 rounded-lg max-w-md w-full shadow-2xl border border-gray-700" | |||||
@click.stop | |||||
> | |||||
<div class="flex items-center justify-between p-6 border-b border-gray-700"> | |||||
<h2 class="text-xl font-semibold text-white">{{ t("support.chat.title") }}</h2> | |||||
<button | |||||
@click="showChatModal = false" | |||||
class="text-gray-400 hover:text-gray-200 transition-colors" | |||||
> | |||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
</div> | |||||
<div class="p-6 text-center"> | |||||
<div class="w-16 h-16 bg-red-500/20 text-red-400 rounded-full flex items-center justify-center mx-auto mb-4"> | |||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-lg font-semibold text-white mb-2">{{ t("support.comingSoon") }}</h3> | |||||
<p class="text-gray-400 text-sm">在线客服功能即将为您开通,敬请期待</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</ErrorBoundary> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* 技术支持页面 | |||||
* 提供专业的技术支持服务入口 | |||||
*/ | |||||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||||
const { error, isLoading } = useErrorHandler(); | |||||
const { t, locale } = useI18n(); | |||||
// 计算首页路径 | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "" : `/${locale.value}`; | |||||
}); | |||||
// 模态框控制 | |||||
const showDocsModal = ref(false); | |||||
const showManualsModal = ref(false); | |||||
const showDriversModal = ref(false); | |||||
const showChatModal = ref(false); | |||||
// SEO优化 | |||||
useHead({ | |||||
title: t("support.title") + " - Hanye", | |||||
meta: [ | |||||
{ | |||||
name: "description", | |||||
content: t("support.description"), | |||||
}, | |||||
{ | |||||
name: "keywords", | |||||
content: t("support.keywords"), | |||||
}, | |||||
], | |||||
}); | |||||
</script> | |||||
<style scoped> | |||||
/* 简洁的悬停效果 */ | |||||
.group:hover { | |||||
transform: translateY(-2px); | |||||
} | |||||
/* 按钮悬停效果 */ | |||||
.hover\:bg-blue-700:hover { | |||||
background-color: #1d4ed8; | |||||
} | |||||
.hover\:bg-gray-600:hover { | |||||
background-color: #4b5563; | |||||
} | |||||
/* 过渡效果 */ | |||||
.transition-all { | |||||
transition-property: all; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.transition-colors { | |||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.transition-transform { | |||||
transition-property: transform; | |||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.duration-200 { | |||||
transition-duration: 200ms; | |||||
} | |||||
/* 专业的阴影效果 */ | |||||
.shadow-lg { | |||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |||||
} | |||||
.shadow-xl { | |||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |||||
} | |||||
.shadow-2xl { | |||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | |||||
} | |||||
/* 响应式优化 */ | |||||
@media (max-width: 768px) { | |||||
.group:hover { | |||||
transform: none; | |||||
} | |||||
} | |||||
</style> |