Files
admin/www/app/components/section/layouts/LayoutSpecTable.vue
T
2026-08-10 14:56:44 +08:00

164 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import type { SectionBlock, SpecGroup } from '~/types/site';
const props = defineProps<{ section: SectionBlock }>();
const { overline, title, subtitle, content, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
const specGroups = computed<SpecGroup[]>(() => props.section.spec_groups ?? []);
// 每个分组的展开状态(默认收起,仅显示前 default_visible 行)
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];
}
// 固定显示的参数项:始终为前 default_visible 项(默认 5 行),
// 其余项由下方 Transition 区域渲染,避免展开时重复
function fixedItems(group: SpecGroup) {
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);
}
// 展开/收起高度过渡:从 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);
}
</script>
<template>
<div class="section-container py-[var(--sec-py,96px)]">
<!-- 区块标题整个区块顶部居中 -->
<div class="text-center max-w-640px mx-auto mb-56px">
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
{{ overline }}
</p>
<h2 class="mt-12px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
{{ title }}
</h2>
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
{{ subtitle }}
</p>
<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" />
</div>
<!-- 参数分组左分组标题 + 右参数卡片 -->
<div class="flex flex-col gap-64px">
<div
v-for="group in specGroups"
:key="group.id"
class="md:grid md:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] gap-64px items-start"
>
<!-- 左栏分组标题桌面端吸顶 -->
<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'">
{{ isEn ? group.title_en : group.title_zh }}
</h3>
</div>
<!-- 右栏参数卡片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>
<span class="flex-1 text-sm text-left" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
{{ 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>
<span class="flex-1 text-sm text-left" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
{{ isEn ? item.value_en : item.value_zh }}
</span>
</div>
</div>
</Transition>
</div>
<!-- 展开/收起按钮 -->
<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>
</div>
</div>
</div>
</div>
</template>