Kaynağa Gözat

feat: 新增支持页面及FAQ功能

- 添加支持页面,提供技术支持服务入口,包含FAQ和联系信息。
- 新增FAQ内容组件,支持分类和搜索功能,提升用户体验。
- 更新国际化文件,增加支持页面相关内容的多语言支持。
- 删除旧的FAQ页面,整合至新的支持页面中,优化代码结构。
master
lizhuang 1 hafta önce
ebeveyn
işleme
9d2f2e34c1

BIN
assets/images/about.webp Dosyayı Görüntüle


+ 472
- 0
components/FaqContent.vue Dosyayı Görüntüle

@@ -0,0 +1,472 @@
<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>

+ 1
- 1
components/TheFooter.vue Dosyayı Görüntüle

@@ -196,7 +196,7 @@ const menuWebsiteItems = computed(() => [
},
{
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",

+ 49
- 16
components/TheHeader.vue Dosyayı Görüntüle

@@ -41,7 +41,11 @@
<div
@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="[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>
</div>
@@ -54,18 +58,24 @@
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>
<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") }}
</h3>
<div class="flex gap-x-32 gap-y-8">
<div class="flex gap-x-40 gap-y-8">
<!-- 企业用户产品组 -->
<div>
<h4 class="text-base font-semibold text-white mb-3">
<h4
class="text-lg font-semibold text-white mb-6"
>
<nuxt-link
:to="`${homePath}products?audiences=1`"
class="flex items-center gap-2 hover:text-cyan-400 transition-colors"
@@ -76,7 +86,9 @@
</h4>
<ul class="space-y-2">
<li
v-for="link in getGroupedItems(item.children[0].items).enterprise"
v-for="link in getGroupedItems(
item.children[0].items
).enterprise"
:key="link.path"
>
<nuxt-link
@@ -86,8 +98,15 @@
>
{{ t(link.label) }}
</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
:to="`${link.path}&tag=${tag}`"
@click.stop="handleMouseLeave"
@@ -103,7 +122,9 @@

<!-- 个人用户产品组 -->
<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
:to="`${homePath}products?audiences=0`"
class="flex items-center gap-2 hover:text-cyan-400 transition-colors"
@@ -112,9 +133,11 @@
<span>{{ t("home.personal.title") }}</span>
</nuxt-link>
</h4>
<ul class="flex gap-12">
<ul class="flex gap-24">
<li
v-for="link in getGroupedItems(item.children[0].items).personal"
v-for="link in getGroupedItems(
item.children[0].items
).personal"
:key="link.path"
>
<nuxt-link
@@ -124,8 +147,15 @@
>
{{ t(link.label) }}
</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
:to="`${link.path}&tag=${tag}`"
@click.stop="handleMouseLeave"
@@ -556,7 +586,7 @@ const menuItems = computed(() => {
},
],
},
{ label: "common.faq", path: `${prefix}/faq` },
{ label: "support.title", path: `${prefix}/support` },
{ label: "common.about", path: `${prefix}/about` },
{ label: "common.contact", path: `${prefix}/contact` },
];
@@ -863,7 +893,11 @@ header {

/* 添加下拉菜单背景渐变 */
.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)
);
}

/* 响应式布局 */
@@ -911,4 +945,3 @@ header {
}
}
</style>


+ 39
- 1
i18n/locales/en.ts Dosyayı Görüntüle

@@ -185,6 +185,44 @@ export default {
category: "Category",
noResults: "No results found",
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: {
title: "About Us",
@@ -283,7 +321,7 @@ export default {
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.",
support: {
title: "Technical Support",
title: "Support",
item1: "Professional technical consultation",
item2: "Optimal solution proposals",
item3: "Technical training",

+ 38
- 0
i18n/locales/ja.ts Dosyayı Görüntüle

@@ -177,6 +177,44 @@ export default {
category: "カテゴリー",
noResults: "該当する質問はありません",
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: {
title: "当社について - Hanye",

+ 38
- 0
i18n/locales/zh.ts Dosyayı Görüntüle

@@ -174,6 +174,44 @@ export default {
category: "分类",
clearSearch: "清除搜索",
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: {
title: "关于我们",

+ 68
- 7
pages/about.vue Dosyayı Görüntüle

@@ -80,10 +80,13 @@
<!-- 关于我们 (公司简介) -->
<div
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"
>
<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">
<h2
class="section-title text-[#35F1FF] text-sm font-bold mb-4 tracking-[0.2em] animate-slide-in-left"
@@ -637,13 +640,13 @@ onMounted(() => {
// 使用 Intersection Observer 优化性能和准确性
const observerOptions = {
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[]) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (entry.isIntersecting && entry.intersectionRatio > 0.1) {
currentSection.value = entry.target.id;
entry.target.classList.add('animate-in');
}
@@ -662,7 +665,16 @@ onMounted(() => {
contactRef.value
];
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');
}
}
});
});

@@ -674,9 +686,30 @@ onMounted(() => {
});
});

// 处理滚动 (保留用于横幅视差效果)
// 处理滚动 (保留用于横幅视差效果和备用动画触发)
const handleLegacyScroll = () => {
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优化
@@ -696,6 +729,34 @@ useHead({
</script>

<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 {
opacity: 0;

+ 0
- 622
pages/faq.vue Dosyayı Görüntüle

@@ -1,622 +0,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>

+ 1
- 1
pages/index.vue Dosyayı Görüntüle

@@ -245,7 +245,7 @@
<nuxt-link
v-for="(recommend, index) in recommendList"
:key="index"
:to="`${homepagePath}products/${recommend.name}`"
:to="`${homepagePath}/products/${recommend.name}`"
class="w-full h-full"
>
<div

+ 0
- 68
pages/support.vue Dosyayı Görüntüle

@@ -1,68 +0,0 @@
<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>

+ 170
- 0
pages/support/faq.vue Dosyayı Görüntüle

@@ -0,0 +1,170 @@
<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>

+ 456
- 0
pages/support/index.vue Dosyayı Görüntüle

@@ -0,0 +1,456 @@
<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>

+ 1
- 0
public/files/E5P-PCIe5.pdf Dosyayı Görüntüle

@@ -0,0 +1 @@

Loading…
İptal
Kaydet