fix: 时间显示/3D线置顶强调/子标签页重构

1. fix(HeroSection): period 为 {zh,en} 对象却直接渲染导致显示 [object Object],
   改为字符串 '2022'
2. feat(timeline): 3D滚球(最终形态)线移到最前并 featured 强调(渐变背景+★+实心3D徽章),
   其后用分隔标记提示「早期并行演进:2D线」
3. refactor(MobileGame): 团队+排行榜/迭代记录/实验 改为平级子标签页(SubTabs),
   默认显示团队+排行榜;新增 SubTabs.vue 通用子标签组件
   headless 验证:默认页(团队+排行榜) -> 迭代记录(时间线) -> 实验(视频) -> 切回 全通过,
   中英文子标签页文案正常
This commit is contained in:
Zikai
2026-07-12 15:34:30 +00:00
parent 0adb320a47
commit 8e08e619fc
5 changed files with 163 additions and 60 deletions

View File

@@ -1,7 +1,9 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import HeroSection from './components/HeroSection.vue'
import ScreenshotCarousel from './components/ScreenshotCarousel.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'
@@ -11,6 +13,14 @@ const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
const videoPoster = '/448/2.jpg'
const videoSrc = '/448/ml/449ml.mp4'
// 子标签页:默认显示团队+排行榜
const subTabs = [
{ id: 'team', label: { zh: '团队 · 排行榜', en: 'Team · Leaderboard' } },
{ id: 'iterations', label: { zh: '迭代记录', en: 'Iterations' } },
{ id: 'experiment', label: { zh: '实验', en: 'Experiment' } }
]
const activeTab = ref('team')
</script>
<template>
@@ -18,7 +28,7 @@ const videoSrc = '/448/ml/449ml.mp4'
<HeroSection />
<div class="container">
<!-- 概览 + 截图轮播 -->
<!-- 概览 + 截图轮播始终显示 -->
<section class="block">
<div class="block__grid">
<div class="block__text">
@@ -45,46 +55,51 @@ const videoSrc = '/448/ml/449ml.mp4'
</div>
</section>
<!-- 版本时间线 -->
<!-- 子标签页区域 -->
<section class="block">
<h2 class="section-title">
<small>{{ L('版本演进', 'Version History') }}</small>
{{ L('迭代记录', 'Iteration Record') }}
</h2>
<VersionTimeline />
</section>
<SubTabs v-model="activeTab" :tabs="subTabs" />
<!-- 双列团队 + 排行榜 -->
<section class="block block--two">
<div>
<h2 class="section-title">
<small>{{ L('团队', 'Team') }}</small>
{{ L('成员', 'Members') }}
</h2>
<TeamCard />
<!-- 团队 + 排行榜默认 -->
<div v-show="activeTab === 'team'" class="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>
<div>
<h2 class="section-title">
<small>{{ L('排行榜', 'Leaderboard') }}</small>
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
</h2>
<Leaderboard />
</div>
</section>
<!-- ML-Agents 演示视频 -->
<section class="block">
<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 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>
</div>

View File

@@ -7,7 +7,7 @@ const { locale } = useI18n({ useScope: 'global' })
const meta = {
course: { zh: '迈阿密大学 · 毕设 448 / 449', en: 'Miami University · Capstone 448 / 449' },
stack: { zh: 'Unity · C# · WebGL · Android · PHP', en: 'Unity · C# · WebGL · Android · PHP' },
period: { zh: '2022', en: '2022' },
period: '2022',
role: { zh: '主开发', en: 'Lead Developer' }
}
</script>

View File

@@ -0,0 +1,66 @@
<script setup>
import { useI18n } from 'vue-i18n'
const props = defineProps({
tabs: { type: Array, required: true }, // [{ id, label: {zh,en} }]
modelValue: { type: String, required: true }
})
const emit = defineEmits(['update:modelValue'])
const { locale } = useI18n({ useScope: 'global' })
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
</script>
<template>
<div class="subtabs">
<button
v-for="t in tabs"
:key="t.id"
type="button"
class="subtabs__btn"
:class="{ 'is-active': modelValue === t.id }"
@click="emit('update:modelValue', t.id)"
>
{{ L(t.label) }}
</button>
</div>
</template>
<style scoped>
.subtabs {
display: flex;
gap: var(--space-xs);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-lg);
overflow-x: auto;
scrollbar-width: none;
}
.subtabs::-webkit-scrollbar {
display: none;
}
.subtabs__btn {
position: relative;
padding: var(--space-sm) var(--space-md);
font-size: 0.92rem;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
transition: color var(--transition);
}
.subtabs__btn:hover {
color: var(--text-primary);
}
.subtabs__btn.is-active {
color: var(--accent);
}
.subtabs__btn.is-active::after {
content: '';
position: absolute;
left: var(--space-md);
right: var(--space-md);
bottom: -1px;
height: 2px;
background: var(--accent);
border-radius: 2px;
}
</style>

View File

@@ -6,7 +6,7 @@ import { versions, tracks } from '../data/versions.js'
const { locale } = useI18n({ useScope: 'global' })
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
// 按演进线分组;同一线内按 order 排序;线之间按各自最早 order 排序
// 按 tracks 数组顺序分组3D 最终形态在前);同一线内按 order 排序
const grouped = computed(() => {
return tracks
.map((t) => {
@@ -16,11 +16,13 @@ const grouped = computed(() => {
return { track: t, items }
})
.filter((g) => g.items.length > 0)
.sort((a, b) => Math.min(...a.items.map((i) => i.order)) - Math.min(...b.items.map((i) => i.order)))
})
// 标记 3D 转折3D 线之前的最后一条 2D 线结尾,提示形态转变
const is3D = (track) => track.form === '3D'
const isFeatured = (track) => track.featured
// 形态转变标记:出现在 3D 线首位之后、2D 线之前
const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grouped.value[0].track)
</script>
<template>
@@ -29,12 +31,16 @@ const is3D = (track) => track.form === '3D'
v-for="(g, gi) in grouped"
:key="g.track.id"
class="timeline__track"
:class="{ 'timeline__track--3d': is3D(g.track) }"
:class="{
'timeline__track--3d': is3D(g.track),
'timeline__track--featured': isFeatured(g.track)
}"
>
<!-- 线标题 -->
<div class="timeline__track-head">
<span class="timeline__form" :class="{ 'is-3d': is3D(g.track) }">{{ g.track.form }}</span>
<span class="timeline__track-name">{{ L(g.track.name) }}</span>
<span v-if="isFeatured(g.track)" class="timeline__star"></span>
</div>
<p class="timeline__track-desc">{{ L(g.track.desc) }}</p>
@@ -78,11 +84,11 @@ const is3D = (track) => track.form === '3D'
</div>
</div>
<!-- 2D -> 3D 转折标记仅出现在第一条 3D 线之前 -->
<div v-if="is3D(g.track) && gi > 0" class="timeline__pivot">
<!-- 形态转变标记3D 线之后2D 线之前 -->
<div v-if="showPivotAfter(gi)" class="timeline__pivot">
<span class="timeline__pivot-line"></span>
<span class="timeline__pivot-text">
{{ locale === 'zh-CN' ? '形态转变2D → 3D 滚球' : 'Form shift: 2D → 3D rolling ball' }}
{{ locale === 'zh-CN' ? '早期并行演进2D 线' : 'Earlier parallel lines: 2D' }}
</span>
<span class="timeline__pivot-line"></span>
</div>
@@ -96,8 +102,19 @@ const is3D = (track) => track.form === '3D'
flex-direction: column;
gap: var(--space-xl);
}
.timeline__track--3d {
padding-top: var(--space-sm);
/* 最终形态强调 */
.timeline__track--featured {
padding: var(--space-lg);
background: linear-gradient(180deg, var(--accent-weak) 0%, transparent 100%);
border: 1px solid var(--accent-weak);
border-radius: var(--radius);
}
.timeline__track--featured .timeline__track-name {
font-size: 1.2rem;
}
.timeline__track--featured .timeline__track-desc {
color: var(--text-primary);
}
.timeline__track-head {
@@ -118,14 +135,18 @@ const is3D = (track) => track.form === '3D'
letter-spacing: 0.04em;
}
.timeline__form.is-3d {
background: var(--accent-weak);
color: var(--accent);
background: var(--accent);
color: #fff;
}
.timeline__track-name {
font-weight: 600;
font-size: 1.05rem;
color: var(--text-primary);
}
.timeline__star {
color: var(--accent);
font-size: 0.9rem;
}
.timeline__track-desc {
font-size: 0.9rem;
color: var(--text-secondary);
@@ -220,7 +241,7 @@ const is3D = (track) => track.form === '3D'
font-size: 0.85rem;
}
/* 2D -> 3D 转折标记 */
/* 形态转变标记 */
.timeline__pivot {
display: flex;
align-items: center;
@@ -235,7 +256,7 @@ const is3D = (track) => track.form === '3D'
.timeline__pivot-text {
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
color: var(--text-tertiary);
white-space: nowrap;
letter-spacing: 0.02em;
}

View File

@@ -9,6 +9,16 @@
// track 字段标识所属演进线form 标识 2D/3Dorder 用于时间线排序(按发布先后)。
export const tracks = [
{
id: 'roll-a-ball',
name: { zh: '3D 滚球(最终形态)', en: '3D Rolling Ball (Final Form)' },
form: '3D',
featured: true,
desc: {
zh: '项目从 2D 转向 3D 的最终形态。手机陀螺仪倾斜操控滚球,含雪花砖掉落、黄色砖弹跳等机制。',
en: 'The final form as the project shifted from 2D to 3D. Tilt the phone to roll the ball; snowflake bricks fall, yellow bricks bounce.'
}
},
{
id: 'zikai-ui',
name: { zh: 'zikai-ui 角色控制器', en: 'zikai-ui Character Controller' },
@@ -35,15 +45,6 @@ export const tracks = [
zh: '2D 横版平台跳跃,吃金币、计分、血量、触屏摇杆。',
en: '2D side-scrolling platformer: collect coins, score, health, touch joystick.'
}
},
{
id: 'roll-a-ball',
name: { zh: '3D 滚球(最终形态)', en: '3D Rolling Ball (Final Form)' },
form: '3D',
desc: {
zh: '项目从 2D 转向 3D 的最终形态。手机陀螺仪倾斜操控滚球,含雪花砖掉落、黄色砖弹跳等机制。',
en: 'The final form as the project shifted from 2D to 3D. Tilt the phone to roll the ball; snowflake bricks fall, yellow bricks bounce.'
}
}
]