feat: init mainPage portfolio site

Vue 3 模块化作品集主页,浅色极简风格,中英双语懒加载 i18n。

- 模块注册表(import.meta.glob)约定式收集项目模块,新增页签无需改导航/路由
- i18n: 中文同步注入主 chunk,英文动态 import() 拆独立 chunk,仅切换时下载
- 第一个页签 mobile-game: 展示 448/449 移动游戏毕设(概览/截图轮播/版本时间线/团队/排行榜/ML视频)
- 排行榜复用 /449/449rest.php,失败优雅降级
- 构建: vite outDir=/root/html,emptyOutDir=false,产物用 mainPage/ 命名空间
- 样式: CSS 变量令牌,无 UI 框架运行时依赖
This commit is contained in:
Zikai
2026-07-12 14:18:35 +00:00
commit 15af291da0
31 changed files with 2927 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { screenshots } from '../data/versions.js'
const { locale } = useI18n()
const current = ref(0)
let timer = null
function go(i) {
current.value = (i + screenshots.length) % screenshots.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="(s, i) in screenshots" :key="i" class="carousel__slide">
<img :src="s.src" :alt="locale === 'zh-CN' ? s.caption.zh : s.caption.en" 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="(s, i) in screenshots"
: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>