fix: EN切换失效、删除贡献者描述、版本按演进线分组(2D->3D)

- fix(i18n): composition 模式下响应式 locale 在 i18n.global 上(非 i18n.locale),
  loadLocaleAsync 与 useI18nLazy 改用 i18n.global.locale;
  useI18nLazy.current 改为派生自全局 locale 的 computed(单一数据源);
  各组件 useI18n() 加 useScope:'global' 确保读到同一 composer。
  headless 验证:默认中文(不下载EN chunk)->点EN加载en.[hash].js并切英文->切回中文 全通过
- content: 删除 MobileGame 概览中「主要贡献者 Zikai Wang... Hongxiang He...」句子
- timeline: 版本数据改为多演进线并行(zikai-ui/main/doby/roll-a-ball),
  每线带 2D/3D 形态标记,3D 线前显示「形态转变:2D -> 3D 滚球」转折标记
This commit is contained in:
Zikai
2026-07-12 15:03:04 +00:00
parent 15af291da0
commit e8dee338ed
11 changed files with 273 additions and 105 deletions

View File

@@ -2,7 +2,7 @@
import Tag from '../../../components/ui/Tag.vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const meta = {
course: { zh: '迈阿密大学 · 毕设 448 / 449', en: 'Miami University · Capstone 448 / 449' },

View File

@@ -2,7 +2,7 @@
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const rows = ref([])
const state = ref('loading') // loading | ok | error

View File

@@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { screenshots } from '../data/versions.js'
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const current = ref(0)
let timer = null

View File

@@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n'
import { team } from '../data/team.js'
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
</script>

View File

@@ -1,49 +1,90 @@
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { versions } from '../data/versions.js'
import { versions, tracks } from '../data/versions.js'
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
// 按演进线分组;同一线内按 order 排序;线之间按各自最早 order 排序
const grouped = computed(() => {
return tracks
.map((t) => {
const items = versions
.filter((v) => v.track === t.id)
.sort((a, b) => a.order - b.order)
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'
</script>
<template>
<div class="timeline">
<div
v-for="(v, i) in versions"
:key="v.id"
class="timeline__item"
v-for="(g, gi) in grouped"
:key="g.track.id"
class="timeline__track"
:class="{ 'timeline__track--3d': is3D(g.track) }"
>
<div class="timeline__marker">
<span class="timeline__dot"></span>
<span v-if="i < versions.length - 1" class="timeline__line"></span>
<!-- 线标题 -->
<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>
</div>
<p class="timeline__track-desc">{{ L(g.track.desc) }}</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">{{ locale === 'zh-CN' ? '操作' : 'Controls' }}</span>
<span>{{ L(v.control) }}</span>
</div>
<div class="timeline__row">
<span class="timeline__label">{{ locale === 'zh-CN' ? '说明' : 'Note' }}</span>
<span>{{ L(v.note) }}</span>
</div>
<div class="timeline__row">
<span class="timeline__label">Unity</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">
{{ locale === 'zh-CN' ? 'WebGL 试玩' : 'Play WebGL' }}
</a>
<a v-if="v.apk" :href="v.apk" target="_blank" rel="noopener">
{{ locale === 'zh-CN' ? '下载 APK' : 'Download APK' }}
</a>
</div>
</div>
</div>
</div>
<div class="timeline__body">
<div class="timeline__head">
<span class="timeline__series">{{ v.series }}</span>
<span class="timeline__version">v{{ v.version }}</span>
</div>
<div class="timeline__product">{{ v.product }}</div>
<div class="timeline__row">
<span class="timeline__label">{{ locale === 'zh-CN' ? '操作' : 'Controls' }}</span>
<span>{{ L(v.control) }}</span>
</div>
<div class="timeline__row">
<span class="timeline__label">{{ locale === 'zh-CN' ? '说明' : 'Note' }}</span>
<span>{{ L(v.note) }}</span>
</div>
<div class="timeline__row">
<span class="timeline__label">Unity</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">
{{ locale === 'zh-CN' ? 'WebGL 试玩' : 'Play WebGL' }}
</a>
<a v-if="v.apk" :href="v.apk" target="_blank" rel="noopener">
{{ locale === 'zh-CN' ? '下载 APK' : 'Download APK' }}
</a>
</div>
<!-- 2D -> 3D 转折标记仅出现在第一条 3D 线之前 -->
<div v-if="is3D(g.track) && gi > 0" 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' }}
</span>
<span class="timeline__pivot-line"></span>
</div>
</div>
</div>
@@ -51,42 +92,87 @@ const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
<style scoped>
.timeline {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-xl);
}
.timeline__track--3d {
padding-top: var(--space-sm);
}
.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-weak);
color: var(--accent);
}
.timeline__track-name {
font-weight: 600;
font-size: 1.05rem;
color: var(--text-primary);
}
.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-lg);
padding-bottom: var(--space-md);
}
.timeline__marker {
position: relative;
flex: 0 0 16px;
flex: 0 0 14px;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 4px;
padding-top: 6px;
}
.timeline__dot {
width: 12px;
height: 12px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--bg);
border: 2px solid var(--accent);
flex: 0 0 12px;
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: 24px;
min-height: 20px;
}
.timeline__body {
flex: 1;
padding: var(--space-md) var(--space-lg);
padding: var(--space-sm) var(--space-md);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
transition: box-shadow var(--transition);
}
@@ -97,37 +183,32 @@ const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
display: flex;
align-items: baseline;
gap: var(--space-sm);
margin-bottom: 2px;
}
.timeline__series {
font-weight: 600;
color: var(--text-primary);
font-size: 1.02rem;
margin-bottom: var(--space-xs);
}
.timeline__version {
font-family: var(--font-mono);
font-size: 0.85rem;
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.85rem;
font-size: 0.82rem;
color: var(--text-tertiary);
margin-bottom: var(--space-sm);
}
.timeline__row {
display: flex;
gap: var(--space-sm);
font-size: 0.9rem;
font-size: 0.88rem;
color: var(--text-secondary);
padding: 2px 0;
padding: 1px 0;
}
.timeline__label {
flex: 0 0 48px;
flex: 0 0 52px;
color: var(--text-tertiary);
font-size: 0.82rem;
font-size: 0.8rem;
}
.timeline__mono {
font-family: var(--font-mono);
@@ -135,7 +216,27 @@ const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
.timeline__links {
display: flex;
gap: var(--space-md);
margin-top: var(--space-sm);
font-size: 0.88rem;
margin-top: var(--space-xs);
font-size: 0.85rem;
}
/* 2D -> 3D 转折标记 */
.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(--accent);
white-space: nowrap;
letter-spacing: 0.02em;
}
</style>