2026-07-31 19:02:00 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-08-10 14:56:44 +08:00
|
|
|
|
import { Icon } from '@iconify/vue';
|
2026-07-31 19:02:00 +08:00
|
|
|
|
import type { SectionBlock, SpecGroup } from '~/types/site';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ section: SectionBlock }>();
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
const { overline, title, subtitle, content, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
2026-07-31 19:02:00 +08:00
|
|
|
|
|
|
|
|
|
|
const specGroups = computed<SpecGroup[]>(() => props.section.spec_groups ?? []);
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
// 每个分组的展开状态(默认收起,仅显示前 default_visible 行)
|
2026-07-31 19:02:00 +08:00
|
|
|
|
const expandedMap = ref<Record<number, boolean>>({});
|
|
|
|
|
|
|
|
|
|
|
|
function isExpanded(group: SpecGroup): boolean {
|
|
|
|
|
|
return !!expandedMap.value[group.id];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggle(group: SpecGroup) {
|
|
|
|
|
|
expandedMap.value[group.id] = !expandedMap.value[group.id];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
// 固定显示的参数项:始终为前 default_visible 项(默认 5 行),
|
|
|
|
|
|
// 其余项由下方 Transition 区域渲染,避免展开时重复
|
|
|
|
|
|
function fixedItems(group: SpecGroup) {
|
2026-07-31 19:02:00 +08:00
|
|
|
|
const items = group.items ?? [];
|
|
|
|
|
|
return items.slice(0, group.default_visible || 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 是否需要显示展开按钮(参数项 > 默认显示数量)
|
|
|
|
|
|
function needToggle(group: SpecGroup): boolean {
|
|
|
|
|
|
return (group.items?.length ?? 0) > (group.default_visible || 5);
|
|
|
|
|
|
}
|
2026-08-10 14:56:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 展开/收起高度过渡:从 0 动画到内容自然高度
|
|
|
|
|
|
// 注意:钩子必须声明并调用 done 回调(Vue 按形参个数判断是否显式接管完成时机),
|
|
|
|
|
|
// 否则 leave 结束后元素不会被移除,导致再次展开失效
|
|
|
|
|
|
const SPEC_TOGGLE_MS = 300;
|
|
|
|
|
|
|
|
|
|
|
|
function onEnter(el: Element, done: () => void) {
|
|
|
|
|
|
const html = el as HTMLElement;
|
|
|
|
|
|
html.style.overflow = 'hidden';
|
|
|
|
|
|
html.style.height = '0';
|
|
|
|
|
|
// 强制 reflow 后再切到目标高度,确保过渡生效
|
|
|
|
|
|
void html.offsetHeight;
|
|
|
|
|
|
html.style.transition = `height ${SPEC_TOGGLE_MS}ms ease`;
|
|
|
|
|
|
html.style.height = `${html.scrollHeight}px`;
|
|
|
|
|
|
setTimeout(done, SPEC_TOGGLE_MS);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onAfterEnter(el: Element) {
|
|
|
|
|
|
const html = el as HTMLElement;
|
|
|
|
|
|
html.style.overflow = '';
|
|
|
|
|
|
html.style.height = '';
|
|
|
|
|
|
html.style.transition = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onLeave(el: Element, done: () => void) {
|
|
|
|
|
|
const html = el as HTMLElement;
|
|
|
|
|
|
html.style.overflow = 'hidden';
|
|
|
|
|
|
html.style.height = `${html.scrollHeight}px`;
|
|
|
|
|
|
void html.offsetHeight;
|
|
|
|
|
|
html.style.transition = `height ${SPEC_TOGGLE_MS}ms ease`;
|
|
|
|
|
|
html.style.height = '0';
|
|
|
|
|
|
setTimeout(done, SPEC_TOGGLE_MS);
|
|
|
|
|
|
}
|
2026-07-31 19:02:00 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-08-05 18:36:01 +08:00
|
|
|
|
<div class="section-container py-[var(--sec-py,96px)]">
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<!-- 区块标题:整个区块顶部居中 -->
|
2026-07-31 19:02:00 +08:00
|
|
|
|
<div class="text-center max-w-640px mx-auto mb-56px">
|
2026-08-04 19:18:35 +08:00
|
|
|
|
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
2026-07-31 19:02:00 +08:00
|
|
|
|
{{ overline }}
|
|
|
|
|
|
</p>
|
2026-08-05 18:36:01 +08:00
|
|
|
|
<h2 class="mt-12px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
2026-07-31 19:02:00 +08:00
|
|
|
|
{{ title }}
|
|
|
|
|
|
</h2>
|
2026-08-03 19:09:03 +08:00
|
|
|
|
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
2026-07-31 19:02:00 +08:00
|
|
|
|
{{ subtitle }}
|
|
|
|
|
|
</p>
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<div v-if="content" class="rich-text mt-20px text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
2026-07-31 19:02:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<!-- 参数分组:左分组标题 + 右参数卡片 -->
|
|
|
|
|
|
<div class="flex flex-col gap-64px">
|
2026-07-31 19:02:00 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="group in specGroups"
|
|
|
|
|
|
:key="group.id"
|
2026-08-10 14:56:44 +08:00
|
|
|
|
class="md:grid md:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] gap-64px items-start"
|
2026-07-31 19:02:00 +08:00
|
|
|
|
>
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<!-- 左栏:分组标题(桌面端吸顶) -->
|
|
|
|
|
|
<div class="mb-24px md:sticky md:top-120px md:mb-0">
|
|
|
|
|
|
<h3 class="text-xl md:text-24px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
2026-07-31 19:02:00 +08:00
|
|
|
|
{{ isEn ? group.title_en : group.title_zh }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<!-- 右栏:参数卡片(2 列:参数名 / 参数值) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="rounded-16px overflow-hidden"
|
|
|
|
|
|
:class="isDark ? 'bg-white/5 border border-white/10' : 'bg-gray-100 border border-gray-200 dark:bg-dark-800 dark:border-dark-700'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="px-24px md:px-32px py-16px">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(item, idx) in fixedItems(group)"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
|
|
|
|
|
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', idx === fixedItems(group).length - 1 && (!needToggle(group) || !isExpanded(group)) ? 'border-b-0' : '']"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="text-sm font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
|
|
|
|
|
{{ isEn ? item.name_en : item.name_zh }}
|
|
|
|
|
|
</span>
|
2026-08-10 18:50:14 +08:00
|
|
|
|
<span class="flex-1 text-sm text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
2026-08-10 14:56:44 +08:00
|
|
|
|
{{ isEn ? item.value_en : item.value_zh }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 收起时隐藏的参数项:高度过渡动画 -->
|
|
|
|
|
|
<Transition @enter="onEnter" @after-enter="onAfterEnter" @leave="onLeave">
|
|
|
|
|
|
<div v-if="isExpanded(group)">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in group.items?.slice(group.default_visible || 5)"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
|
|
|
|
|
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', item.id === group.items?.at(-1)?.id ? 'border-b-0' : '']"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="text-sm font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
|
|
|
|
|
{{ isEn ? item.name_en : item.name_zh }}
|
|
|
|
|
|
</span>
|
2026-08-10 18:50:14 +08:00
|
|
|
|
<span class="flex-1 text-sm text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
2026-08-10 14:56:44 +08:00
|
|
|
|
{{ isEn ? item.value_en : item.value_zh }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
2026-07-31 19:02:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-10 14:56:44 +08:00
|
|
|
|
<!-- 展开/收起按钮 -->
|
|
|
|
|
|
<div v-if="needToggle(group)" class="px-24px md:px-32px py-16px flex justify-center">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="text-sm font-medium inline-flex items-center gap-8px bg-transparent cursor-pointer transition-colors"
|
|
|
|
|
|
:class="isDark ? 'text-white' : 'text-gray-600 dark:text-white'"
|
|
|
|
|
|
@click="toggle(group)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template v-if="isExpanded(group)">
|
|
|
|
|
|
{{ isEn ? 'View less' : '收起参数' }}
|
|
|
|
|
|
<span class="w-24px h-24px rounded-full flex items-center justify-center text-14px" :class="isDark ? 'bg-white/10 text-white' : 'bg-gray-200 text-gray-700'" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="mingcute:up-line" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
{{ isEn ? `View all (${group.items?.length ?? 0})` : `查看全部参数(${group.items?.length ?? 0} 项)` }}
|
|
|
|
|
|
<span class="w-24px h-24px rounded-full flex items-center justify-center text-14px" :class="isDark ? 'bg-white/10 text-white' : 'bg-gray-200 text-gray-700'" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="mingcute:down-line" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-07-31 19:02:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|