结构优化:
- 新增 mobile-game/config.js:集中模块静态配置(资源路径、排行榜端点、
hero meta、子标签页定义),消除散落在各组件的硬编码
- 新增 composables/useLocale.js:共享语言辅助(t/isZh/pick),
消除 6 个组件各自重复定义的 L() helper
- 文案全部迁入 i18n locale(mobileGame 命名空间),消除模板内联 L('中','英')
- 数据文件 versions.js 去除 {zh,en} 对象,文案改由 i18n 键(tracks/versions)取
- 各组件改为读 config + t(),不再直接 useI18n,降低与 i18n 实现的耦合
headless 验证:中文(hero/meta/team/timeline/3D置顶) + 英文切换 + 切回中文 全通过
132 lines
2.9 KiB
Vue
132 lines
2.9 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { useLocale } from '../../../composables/useLocale.js'
|
||
import { assets } from '../config.js'
|
||
|
||
const { t } = useLocale()
|
||
const images = assets.screenshots
|
||
const current = ref(0)
|
||
let timer = null
|
||
|
||
function go(i) {
|
||
current.value = (i + images.length) % images.length
|
||
}
|
||
function next() {
|
||
go(current.value + 1)
|
||
}
|
||
function pause() {
|
||
if (timer) {
|
||
clearInterval(timer)
|
||
timer = null
|
||
}
|
||
}
|
||
function resume() {
|
||
if (!timer) timer = setInterval(next, 5000)
|
||
}
|
||
|
||
onMounted(resume)
|
||
onUnmounted(pause)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="carousel" @mouseenter="pause" @mouseleave="resume">
|
||
<div class="carousel__viewport">
|
||
<div class="carousel__track" :style="{ transform: `translateX(-${current * 100}%)` }">
|
||
<figure v-for="(src, i) in images" :key="i" class="carousel__slide">
|
||
<img :src="src" :alt="t('mobileGame.screenshots.caption', { n: i + 1 })" loading="lazy" />
|
||
</figure>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="carousel__btn carousel__btn--prev" @click="go(current - 1)" aria-label="prev">‹</button>
|
||
<button class="carousel__btn carousel__btn--next" @click="go(current + 1)" aria-label="next">›</button>
|
||
|
||
<div class="carousel__dots">
|
||
<button
|
||
v-for="(src, i) in images"
|
||
:key="i"
|
||
class="carousel__dot"
|
||
:class="{ 'is-active': i === current }"
|
||
@click="go(i)"
|
||
:aria-label="`slide ${i + 1}`"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.carousel {
|
||
position: relative;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
background: var(--surface);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.carousel__viewport {
|
||
overflow: hidden;
|
||
aspect-ratio: 4 / 3;
|
||
}
|
||
.carousel__track {
|
||
display: flex;
|
||
height: 100%;
|
||
transition: transform 0.5s ease;
|
||
}
|
||
.carousel__slide {
|
||
flex: 0 0 100%;
|
||
height: 100%;
|
||
margin: 0;
|
||
}
|
||
.carousel__slide img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.carousel__btn {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-primary);
|
||
font-size: 1.3rem;
|
||
line-height: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: var(--shadow);
|
||
transition: all var(--transition);
|
||
}
|
||
.carousel__btn:hover {
|
||
background: #fff;
|
||
}
|
||
.carousel__btn--prev { left: 12px; }
|
||
.carousel__btn--next { right: 12px; }
|
||
|
||
.carousel__dots {
|
||
position: absolute;
|
||
bottom: 12px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.carousel__dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
border: 1px solid var(--border);
|
||
transition: all var(--transition);
|
||
}
|
||
.carousel__dot.is-active {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
width: 22px;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|