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:
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