perf: 子标签页按需加载(代码与元素懒加载)
原 v-show + 静态 import 导致三个子标签页的组件代码全部打进 MobileGame chunk,且首屏即渲染全部(排行榜 onMounted 立即发 AJAX、视频元数据加载)。 改为: - 三个标签页内容拆为独立组件 TeamLeaderboardTab/IterationsTab/ExperimentTab - MobileGame 用 defineAsyncComponent + 动态 import,Vite 拆成独立 chunk - v-if + Suspense:未激活的标签页组件根本不挂载(不发 AJAX、不渲染) 构建产物可见三个独立 chunk;headless 验证:默认团队页仅加载 team chunk, 点迭代记录才加载 IterationsTab chunk,点实验才加载 ExperimentTab chunk。
This commit is contained in:
@@ -1,18 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, defineAsyncComponent } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import HeroSection from './components/HeroSection.vue'
|
import HeroSection from './components/HeroSection.vue'
|
||||||
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
||||||
import SubTabs from './components/SubTabs.vue'
|
import SubTabs from './components/SubTabs.vue'
|
||||||
import VersionTimeline from './components/VersionTimeline.vue'
|
|
||||||
import TeamCard from './components/TeamCard.vue'
|
|
||||||
import Leaderboard from './components/Leaderboard.vue'
|
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
||||||
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
|
|
||||||
const videoPoster = '/448/2.jpg'
|
|
||||||
const videoSrc = '/448/ml/449ml.mp4'
|
|
||||||
|
|
||||||
// 子标签页:默认显示团队+排行榜
|
// 子标签页:默认显示团队+排行榜
|
||||||
const subTabs = [
|
const subTabs = [
|
||||||
@@ -21,6 +15,13 @@ const subTabs = [
|
|||||||
{ id: 'experiment', label: { zh: '实验', en: 'Experiment' } }
|
{ id: 'experiment', label: { zh: '实验', en: 'Experiment' } }
|
||||||
]
|
]
|
||||||
const activeTab = ref('team')
|
const activeTab = ref('team')
|
||||||
|
|
||||||
|
// ★ 按需加载:三个子标签页用 defineAsyncComponent 动态 import,
|
||||||
|
// 被 Vite/Rollup 拆成独立 chunk,只有切到对应标签页时才下载与渲染。
|
||||||
|
// 配合 v-if,未激活的标签页组件根本不挂载(不执行 setup、不发 AJAX)。
|
||||||
|
const TeamLeaderboardTab = defineAsyncComponent(() => import('./components/TeamLeaderboardTab.vue'))
|
||||||
|
const IterationsTab = defineAsyncComponent(() => import('./components/IterationsTab.vue'))
|
||||||
|
const ExperimentTab = defineAsyncComponent(() => import('./components/ExperimentTab.vue'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,52 +56,18 @@ const activeTab = ref('team')
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 子标签页区域 -->
|
<!-- 子标签页区域:按需加载,v-if 确保未激活的不渲染 -->
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<SubTabs v-model="activeTab" :tabs="subTabs" />
|
<SubTabs v-model="activeTab" :tabs="subTabs" />
|
||||||
|
|
||||||
<!-- 团队 + 排行榜(默认) -->
|
<Suspense>
|
||||||
<div v-show="activeTab === 'team'" class="block--two">
|
<TeamLeaderboardTab v-if="activeTab === 'team'" />
|
||||||
<div>
|
<IterationsTab v-else-if="activeTab === 'iterations'" />
|
||||||
<h2 class="section-title">
|
<ExperimentTab v-else-if="activeTab === 'experiment'" />
|
||||||
<small>{{ L('团队', 'Team') }}</small>
|
<template #fallback>
|
||||||
{{ L('成员', 'Members') }}
|
<div class="tab-loading">{{ L('加载中…', 'Loading…') }}</div>
|
||||||
</h2>
|
</template>
|
||||||
<TeamCard />
|
</Suspense>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="section-title">
|
|
||||||
<small>{{ L('排行榜', 'Leaderboard') }}</small>
|
|
||||||
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
|
|
||||||
</h2>
|
|
||||||
<Leaderboard />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 迭代记录 -->
|
|
||||||
<div v-show="activeTab === 'iterations'">
|
|
||||||
<h2 class="section-title">
|
|
||||||
<small>{{ L('版本演进', 'Version History') }}</small>
|
|
||||||
{{ L('迭代记录', 'Iteration Record') }}
|
|
||||||
</h2>
|
|
||||||
<VersionTimeline />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 实验 -->
|
|
||||||
<div v-show="activeTab === 'experiment'">
|
|
||||||
<h2 class="section-title">
|
|
||||||
<small>{{ L('实验', 'Experiment') }}</small>
|
|
||||||
{{ L('ML-Agents 强化学习演示', 'ML-Agents Reinforcement Learning Demo') }}
|
|
||||||
</h2>
|
|
||||||
<div class="video-wrap">
|
|
||||||
<video controls preload="none" :poster="videoPoster">
|
|
||||||
<source :src="videoSrc" type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
<p class="video-caption">
|
|
||||||
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,36 +96,15 @@ const activeTab = ref('team')
|
|||||||
.block__text p:last-child {
|
.block__text p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.block--two {
|
.tab-loading {
|
||||||
display: grid;
|
padding: var(--space-xl);
|
||||||
grid-template-columns: 1fr 1fr;
|
text-align: center;
|
||||||
gap: var(--space-xl);
|
color: var(--text-tertiary);
|
||||||
align-items: start;
|
font-size: 0.9rem;
|
||||||
}
|
|
||||||
|
|
||||||
.video-wrap {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--surface);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
.video-wrap video {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
.video-caption {
|
|
||||||
padding: var(--space-md) var(--space-lg);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.block__grid,
|
.block__grid {
|
||||||
.block--two {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/modules/mobile-game/components/ExperimentTab.vue
Normal file
48
src/modules/mobile-game/components/ExperimentTab.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
|
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
||||||
|
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
|
||||||
|
const videoPoster = '/448/2.jpg'
|
||||||
|
const videoSrc = '/448/ml/449ml.mp4'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tab-experiment">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<small>{{ L('实验', 'Experiment') }}</small>
|
||||||
|
{{ L('ML-Agents 强化学习演示', 'ML-Agents Reinforcement Learning Demo') }}
|
||||||
|
</h2>
|
||||||
|
<div class="video-wrap">
|
||||||
|
<video controls preload="none" :poster="videoPoster">
|
||||||
|
<source :src="videoSrc" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
<p class="video-caption">
|
||||||
|
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-wrap {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.video-wrap video {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.video-caption {
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/modules/mobile-game/components/IterationsTab.vue
Normal file
17
src/modules/mobile-game/components/IterationsTab.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import VersionTimeline from './VersionTimeline.vue'
|
||||||
|
|
||||||
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
|
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tab-iterations">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<small>{{ L('版本演进', 'Version History') }}</small>
|
||||||
|
{{ L('迭代记录', 'Iteration Record') }}
|
||||||
|
</h2>
|
||||||
|
<VersionTimeline />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
41
src/modules/mobile-game/components/TeamLeaderboardTab.vue
Normal file
41
src/modules/mobile-game/components/TeamLeaderboardTab.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import TeamCard from './TeamCard.vue'
|
||||||
|
import Leaderboard from './Leaderboard.vue'
|
||||||
|
|
||||||
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
|
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tab-team block--two">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">
|
||||||
|
<small>{{ L('团队', 'Team') }}</small>
|
||||||
|
{{ L('成员', 'Members') }}
|
||||||
|
</h2>
|
||||||
|
<TeamCard />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">
|
||||||
|
<small>{{ L('排行榜', 'Leaderboard') }}</small>
|
||||||
|
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
|
||||||
|
</h2>
|
||||||
|
<Leaderboard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.block--two {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.block--two {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user