123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- <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-20">
- <div class="max-w-screen-2xl mx-auto">
- <nuxt-link
- to="/"
- class="justify-start text-white/60 text-base font-normal"
- >{{ $t("common.home") }}</nuxt-link
- >
- <span class="text-white/60 text-base font-normal px-2"> / </span>
- <nuxt-link to="/faq" class="text-white 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:px-2 lg:px-2 md:px-4 px-4"
- >
- <div class="max-w-screen-2xl mx-auto">
- <div class="w-full grid grid-cols-1 md:grid-cols-10 gap-8 md:gap-2">
- <!-- 左侧分类导航 -->
- <div class="col-span-1 md:col-span-2">
- <div class="flex flex-col gap-4">
- <div class="text-white text-3xl font-medium">
- {{ $t("faq.category") }}
- </div>
- <div class="flex flex-col gap-4 w-fit">
- <div
- v-for="category in categories"
- :key="category"
- @click="handleCategoryFilter(category)"
- class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block"
- :class="{
- 'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white 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-8">
- <!-- 搜索框 -->
- <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="faq.id"
- 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.id)"
- >
- <div class="text-white text-xl font-medium">
- <template
- v-for="(part, i) in highlightKeyword(faq.question)"
- :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': expandedFaqs.includes(faq.id) }"
- >
- ▼
- </div>
- </div>
- <div
- v-if="expandedFaqs.includes(faq.id)"
- class="mt-4 text-white/80 text-base font-normal leading-relaxed"
- >
- <template
- v-for="(part, i) in highlightKeyword(faq.answer)"
- :key="i"
- >
- <span v-if="typeof part === 'string'">{{ part }}</span>
- <component v-else :is="part"></component>
- </template>
- </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";
-
- const { error, isLoading, wrapAsync } = useErrorHandler();
-
- const { t, locale } = useI18n();
-
- // FAQ数据
- interface FAQ {
- id: number;
- category: string;
- question: string;
- answer: string;
- }
-
- const faqs = ref<FAQ[]>([
- {
- id: 1,
- category: "製品について",
- question: "製品の保証期間はどのくらいですか?",
- answer:
- "当社の製品は購入日から1年間の保証期間があります。保証期間内に製品に問題が発生した場合は、無償で修理または交換いたします。",
- },
- {
- id: 2,
- category: "製品について",
- question: "製品の取扱説明書はどこで入手できますか?",
- answer:
- "製品の取扱説明書は、当社ウェブサイトの製品ページからダウンロードできます。また、製品パッケージにも同梱されています。",
- },
- {
- id: 3,
- category: "購入について",
- question: "支払い方法は何がありますか?",
- answer:
- "クレジットカード、銀行振込、コンビニ決済など、様々な支払い方法をご利用いただけます。詳細はお支払いページをご確認ください。",
- },
- {
- id: 4,
- category: "購入について",
- question: "返品・交換は可能ですか?",
- answer:
- "商品到着後7日以内であれば、未使用・未開封の商品に限り返品・交換が可能です。詳細は返品・交換ポリシーをご確認ください。",
- },
- {
- id: 5,
- category: "サポートについて",
- question: "技術サポートはどのように受けられますか?",
- answer:
- "メール、電話、オンラインチャットなど、様々な方法で技術サポートをご利用いただけます。営業時間内であれば、即時対応いたします。",
- },
- {
- id: 6,
- category: "サポートについて",
- question: "修理依頼はどのように行えばよいですか?",
- answer:
- "修理依頼は、当社ウェブサイトの修理依頼フォームからお申し込みください。修理状況はマイページから確認できます。",
- },
- ]);
-
- // 分类列表
- const categories = ref([
- "すべて",
- "製品について",
- "購入について",
- "サポートについて",
- ]);
-
- // 选中的分类
- const selectedCategory = ref("すべて");
-
- // 展开的FAQ ID列表
- const expandedFaqs = ref<number[]>([]);
-
- // 搜索关键词
- const searchTerm = ref("");
- const searchInputRef = ref<HTMLInputElement | null>(null);
-
- // 过滤后的FAQ列表
- const filteredFaqs = computed(() => {
- let result = faqs.value;
- if (selectedCategory.value !== "すべて") {
- result = result.filter(
- (faq: FAQ) => faq.category === selectedCategory.value
- );
- }
- if (searchTerm.value.trim()) {
- const keyword = searchTerm.value.trim().toLowerCase();
- result = result.filter(
- (faq: FAQ) =>
- faq.question.toLowerCase().includes(keyword) ||
- faq.answer.toLowerCase().includes(keyword)
- );
- }
- return result;
- });
-
- /**
- * 高亮显示匹配的关键字
- * @param text 原始文本
- * @returns 高亮后的VNode数组
- */
- function highlightKeyword(text: string): (string | any)[] {
- const keyword = searchTerm.value.trim();
- if (!keyword) return [text];
- // 构建正则,忽略大小写,转义特殊字符
- const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const reg = new RegExp(escaped, "gi");
- const parts = text.split(reg);
- const matches = text.match(reg);
- if (!matches) return [text];
- // 组装高亮
- const 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展开状态
- */
- function toggleFaq(id: number) {
- const index = expandedFaqs.value.indexOf(id);
- if (index === -1) {
- expandedFaqs.value.push(id);
- } else {
- expandedFaqs.value.splice(index, 1);
- }
- }
-
- /**
- * 处理分类筛选
- */
- function handleCategoryFilter(category: string) {
- selectedCategory.value = category;
- }
-
- // 自动展开匹配项
- watch([
- filteredFaqs,
- searchTerm
- ], ([faqs, keyword]: [FAQ[], string]) => {
- if (keyword.trim()) {
- expandedFaqs.value = faqs.map((faq: FAQ) => faq.id);
- } else {
- expandedFaqs.value = [];
- }
- });
-
- 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"),
- },
- ],
- });
- </script>
-
- <style scoped>
- /* 添加过渡动画 */
- .fade-enter-active,
- .fade-leave-active {
- transition: opacity 0.3s;
- }
- .fade-enter-from,
- .fade-leave-to {
- opacity: 0;
- }
- </style>
|