Files
admin/www/app/components/section/layouts/LayoutSpecTable.vue
T

164 lines
7.4 KiB
Vue
Raw Normal View History

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>