useI18n({ useScope: 'global' }) 在嵌入时解析到宿主已安装的 i18n 实例,
而非本子项目实例 -> mobileGame.* 文案回退成 i18n key(显示 ID)。
改为 useLocale composable 直接读本模块级 i18n.global,无论是否 app.use() 都正确。
267 lines
7.1 KiB
Vue
267 lines
7.1 KiB
Vue
<script setup>
|
||
import { computed } from 'vue'
|
||
import { useLocale } from '../composables/useLocale.js'
|
||
import { versions, tracks } from '../data/versions.js'
|
||
|
||
const { t } = useLocale()
|
||
|
||
// 按 tracks 数组顺序分组(3D 最终形态在前);同一线内按 order 排序
|
||
const grouped = computed(() => {
|
||
return tracks
|
||
.map((tr) => {
|
||
const items = versions
|
||
.filter((v) => v.track === tr.id)
|
||
.sort((a, b) => a.order - b.order)
|
||
return { track: tr, items }
|
||
})
|
||
.filter((g) => g.items.length > 0)
|
||
})
|
||
|
||
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)
|
||
|
||
// i18n 取值辅助:track/version 的文案在 locale 的 mobileGame.tracks / mobileGame.versions 下
|
||
const trackName = (trackId) => t(`mobileGame.tracks.${trackId}.name`)
|
||
const trackDesc = (trackId) => t(`mobileGame.tracks.${trackId}.desc`)
|
||
const vControl = (versionId) => t(`mobileGame.versions.${versionId}.control`)
|
||
const vNote = (versionId) => t(`mobileGame.versions.${versionId}.note`)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="timeline">
|
||
<div
|
||
v-for="(g, gi) in grouped"
|
||
:key="g.track.id"
|
||
class="timeline__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">{{ trackName(g.track.id) }}</span>
|
||
<span v-if="isFeatured(g.track)" class="timeline__star">★</span>
|
||
</div>
|
||
<p class="timeline__track-desc">{{ trackDesc(g.track.id) }}</p>
|
||
|
||
<!-- 该线下的版本 -->
|
||
<div class="timeline__items">
|
||
<div
|
||
v-for="(v, i) in g.items"
|
||
:key="v.id"
|
||
class="timeline__item"
|
||
>
|
||
<div class="timeline__marker">
|
||
<span class="timeline__dot"></span>
|
||
<span v-if="i < g.items.length - 1" class="timeline__line"></span>
|
||
</div>
|
||
<div class="timeline__body">
|
||
<div class="timeline__head">
|
||
<span class="timeline__version">v{{ v.version }}</span>
|
||
<span class="timeline__product">{{ v.product }}</span>
|
||
</div>
|
||
<div class="timeline__row">
|
||
<span class="timeline__label">{{ t('mobileGame.iterations.colControls') }}</span>
|
||
<span>{{ vControl(v.id) }}</span>
|
||
</div>
|
||
<div class="timeline__row">
|
||
<span class="timeline__label">{{ t('mobileGame.iterations.colNote') }}</span>
|
||
<span>{{ vNote(v.id) }}</span>
|
||
</div>
|
||
<div class="timeline__row">
|
||
<span class="timeline__label">{{ t('mobileGame.iterations.colUnity') }}</span>
|
||
<span class="timeline__mono">{{ v.unity }}</span>
|
||
</div>
|
||
<div class="timeline__links">
|
||
<a v-if="v.webgl" :href="v.webgl" target="_blank" rel="noopener">
|
||
{{ t('mobileGame.iterations.playWebgl') }} ↗
|
||
</a>
|
||
<a v-if="v.apk" :href="v.apk" target="_blank" rel="noopener">
|
||
{{ t('mobileGame.iterations.downloadApk') }} ↗
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 形态转变标记:3D 线之后、2D 线之前 -->
|
||
<div v-if="showPivotAfter(gi)" class="timeline__pivot">
|
||
<span class="timeline__pivot-line"></span>
|
||
<span class="timeline__pivot-text">{{ t('mobileGame.iterations.pivotText') }}</span>
|
||
<span class="timeline__pivot-line"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.timeline {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-xl);
|
||
}
|
||
|
||
/* 最终形态强调 */
|
||
.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 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
margin-bottom: var(--space-xs);
|
||
}
|
||
.timeline__form {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 10px;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
border-radius: 999px;
|
||
background: var(--surface-2);
|
||
color: var(--text-secondary);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.timeline__form.is-3d {
|
||
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);
|
||
margin: 0 0 var(--space-md);
|
||
max-width: 640px;
|
||
}
|
||
|
||
.timeline__items {
|
||
padding-left: 4px;
|
||
}
|
||
.timeline__item {
|
||
display: flex;
|
||
gap: var(--space-md);
|
||
padding-bottom: var(--space-md);
|
||
}
|
||
.timeline__marker {
|
||
position: relative;
|
||
flex: 0 0 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding-top: 6px;
|
||
}
|
||
.timeline__dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: var(--bg);
|
||
border: 2px solid var(--accent);
|
||
flex: 0 0 10px;
|
||
}
|
||
.timeline__track--3d .timeline__dot {
|
||
background: var(--accent);
|
||
}
|
||
.timeline__line {
|
||
flex: 1;
|
||
width: 2px;
|
||
background: var(--border);
|
||
margin-top: 4px;
|
||
min-height: 20px;
|
||
}
|
||
.timeline__body {
|
||
flex: 1;
|
||
padding: var(--space-sm) var(--space-md);
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow-sm);
|
||
transition: box-shadow var(--transition);
|
||
}
|
||
.timeline__body:hover {
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.timeline__head {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--space-sm);
|
||
margin-bottom: var(--space-xs);
|
||
}
|
||
.timeline__version {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.82rem;
|
||
color: var(--accent);
|
||
background: var(--accent-weak);
|
||
padding: 1px 8px;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
}
|
||
.timeline__product {
|
||
font-size: 0.82rem;
|
||
color: var(--text-tertiary);
|
||
}
|
||
.timeline__row {
|
||
display: flex;
|
||
gap: var(--space-sm);
|
||
font-size: 0.88rem;
|
||
color: var(--text-secondary);
|
||
padding: 1px 0;
|
||
}
|
||
.timeline__label {
|
||
flex: 0 0 52px;
|
||
color: var(--text-tertiary);
|
||
font-size: 0.8rem;
|
||
}
|
||
.timeline__mono {
|
||
font-family: var(--font-mono);
|
||
}
|
||
.timeline__links {
|
||
display: flex;
|
||
gap: var(--space-md);
|
||
margin-top: var(--space-xs);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* 形态转变标记 */
|
||
.timeline__pivot {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-md);
|
||
margin: var(--space-md) 0 0;
|
||
}
|
||
.timeline__pivot-line {
|
||
flex: 1;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--border-strong), transparent);
|
||
}
|
||
.timeline__pivot-text {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
color: var(--text-tertiary);
|
||
white-space: nowrap;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
</style>
|