Files
admin/www/app/pages/support.vue
T

177 lines
7.1 KiB
Vue
Raw Normal View History

2026-08-03 19:09:03 +08:00
<script setup lang="ts">
import type { SupportConfig, FaqCategory, FaqItem, SupportDownload, SupportContactItem } from '~/types/site';
import { parseJson } from '~/utils/parseJson';
const { t, locale } = useI18n();
const localePath = useLocalePath();
const { getPageConfig } = useSiteApi();
// SEO:技术支持
await usePageSeo('support', undefined, `${t('app.name')} - ${t('nav.support')}`);
const config = ref<SupportConfig | null>(null);
const loading = ref(true);
onMounted(async () => {
try {
config.value = await getPageConfig<SupportConfig>('support');
} catch {
config.value = null;
} finally {
loading.value = false;
}
});
// ===== JSON 字段解析 =====
const faqCategories = computed<FaqCategory[]>(() =>
parseJson<FaqCategory[]>(config.value?.s1_faq_categories, [])
);
const downloads = computed<SupportDownload[]>(() =>
parseJson<SupportDownload[]>(config.value?.s2_downloads, [])
);
const contacts = computed<SupportContactItem[]>(() =>
parseJson<SupportContactItem[]>(config.value?.s3_contact, [])
);
// ===== 按语言取值 =====
const heroTitle = computed(() => (locale.value === 'en' ? config.value?.hero_title_en : config.value?.hero_title_zh) || (locale.value === 'en' ? 'Support' : '技术支持'));
const heroSubtitle = computed(() => (locale.value === 'en' ? config.value?.hero_subtitle_en : config.value?.hero_subtitle_zh) || '');
const s4Cta = computed(() => (locale.value === 'en' ? config.value?.s4_cta_en : config.value?.s4_cta_zh) || '');
function catTitle(c: FaqCategory): string {
return locale.value === 'en' ? c.title_en : c.title_zh;
}
function qText(q: FaqItem): string {
return locale.value === 'en' ? q.question_en : q.question_zh;
}
function aText(q: FaqItem): string {
return locale.value === 'en' ? q.answer_en : q.answer_zh;
}
function downloadName(d: SupportDownload): string {
return locale.value === 'en' ? d.name_en : d.name_zh;
}
function contactLabel(c: SupportContactItem): string {
return locale.value === 'en' ? c.label_en : c.label_zh;
}
function contactValue(c: SupportContactItem): string {
return locale.value === 'en' ? c.value_en : c.value_zh;
}
// ===== FAQ 手风琴展开状态(分类 -> 展开的问答 index =====
const openFaq = ref<Record<number, number>>({});
function toggleFaq(catIdx: number, itemIdx: number) {
if (openFaq.value[catIdx] === itemIdx) {
delete openFaq.value[catIdx];
} else {
openFaq.value[catIdx] = itemIdx;
}
}
const hasFaq = computed(() => faqCategories.value.length > 0);
const hasDownloads = computed(() => downloads.value.length > 0);
const hasContacts = computed(() => contacts.value.length > 0);
</script>
<template>
<!-- 内容容器加宽--sec-cw 可继承页内所有 section-container 生效全局默认 1200px 偏窄 -->
<div style="--sec-cw: 1400px">
2026-08-03 19:09:03 +08:00
<PageHero
:title="heroTitle"
:subtitle="heroSubtitle || undefined"
:bg-url="config?.hero_bg_url || undefined"
/>
<div v-if="loading" class="py-80px text-center text-sm text-gray-400">
{{ t('common.loading') }}
</div>
<div v-else class="section-container py-64px md:py-80px flex flex-col gap-64px md:gap-80px">
<!-- S1常见问题 -->
<section v-if="hasFaq">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-32px">{{ t('support.faq') }}</h2>
<div class="flex flex-col gap-24px">
<div v-for="(cat, catIdx) in faqCategories" :key="catIdx">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-16px">{{ catTitle(cat) }}</h3>
<div class="flex flex-col gap-12px">
<div
v-for="(item, itemIdx) in cat.items"
:key="itemIdx"
class="rounded-12px border border-gray-100 dark:border-dark-700 overflow-hidden bg-white dark:bg-dark-800"
>
<button
class="w-full flex items-center justify-between gap-16px px-20px py-16px text-left hover:bg-gray-50 dark:hover:bg-dark-700 transition-colors"
@click="toggleFaq(catIdx, itemIdx)"
>
<span class="font-medium text-gray-900 dark:text-white">{{ qText(item) }}</span>
<span
class="text-gray-400 transition-transform duration-200 shrink-0"
:class="openFaq[catIdx] === itemIdx ? 'rotate-180' : ''"
></span>
</button>
<div v-if="openFaq[catIdx] === itemIdx" class="px-20px pb-16px text-sm text-gray-600 dark:text-gray-300 leading-relaxed whitespace-pre-line">
{{ aText(item) }}
</div>
</div>
</div>
</div>
</div>
</section>
<!-- S2资料下载 -->
<section v-if="hasDownloads">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-32px">{{ t('support.downloads') }}</h2>
<div class="grid gap-16px sm:grid-cols-2 lg:grid-cols-3">
<a
v-for="(d, idx) in downloads"
:key="idx"
:href="d.file_url"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-16px p-20px rounded-12px border border-gray-100 dark:border-dark-700 bg-white dark:bg-dark-800 hover:border-primary hover:shadow-md transition-all"
>
<span class="text-2xl">📄</span>
<div class="flex flex-col gap-4px min-w-0 flex-1">
<span class="font-medium text-gray-900 dark:text-white truncate">{{ downloadName(d) }}</span>
<span v-if="d.file_size" class="text-xs text-gray-400">{{ d.file_size }}</span>
</div>
<span class="text-primary text-lg shrink-0"></span>
</a>
</div>
</section>
<!-- S3联系方式 -->
<section v-if="hasContacts">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-32px">{{ t('support.contact') }}</h2>
<div class="grid gap-16px sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="(c, idx) in contacts"
:key="idx"
class="p-20px rounded-12px border border-gray-100 dark:border-dark-700 bg-white dark:bg-dark-800"
>
<p class="text-xs text-gray-400 mb-8px">{{ contactLabel(c) }}</p>
<p class="font-medium text-gray-900 dark:text-white break-all">{{ contactValue(c) }}</p>
</div>
</div>
</section>
<!-- S-FB问题反馈提交 + 查询 -->
<SupportFeedback />
2026-08-03 19:09:03 +08:00
<!-- S4CTA -->
<section v-if="s4Cta" class="text-center py-32px">
<NuxtLink
:to="localePath(config?.s4_cta_url || '/contact')"
class="inline-block px-36px py-14px rounded-8px bg-primary text-white font-medium hover:opacity-90 transition-opacity"
>
{{ s4Cta }}
</NuxtLink>
</section>
<!-- 空配置提示 -->
<div v-if="!hasFaq && !hasDownloads && !hasContacts && !s4Cta" class="py-80px text-center">
<p class="text-gray-400">{{ t('common.empty') }}</p>
</div>
</div>
</div>
</template>