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:
@@ -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,17 +55,12 @@ 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 v-show="activeTab === 'team'" class="block--two">
|
||||
<div>
|
||||
<h2 class="section-title">
|
||||
<small>{{ L('团队', 'Team') }}</small>
|
||||
@@ -70,10 +75,19 @@ const videoSrc = '/448/ml/449ml.mp4'
|
||||
</h2>
|
||||
<Leaderboard />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ML-Agents 演示视频 -->
|
||||
<section class="block">
|
||||
<!-- 迭代记录 -->
|
||||
<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') }}
|
||||
@@ -86,6 +100,7 @@ const videoSrc = '/448/ml/449ml.mp4'
|
||||
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
66
src/modules/mobile-game/components/SubTabs.vue
Normal file
66
src/modules/mobile-game/components/SubTabs.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
// track 字段标识所属演进线;form 标识 2D/3D;order 用于时间线排序(按发布先后)。
|
||||
|
||||
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.'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user