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:
58
src/components/LanguageSwitcher.vue
Normal file
58
src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import { useI18nLazy } from '../composables/useI18nLazy.js'
|
||||
|
||||
const { current, switching, switchLocale } = useI18nLazy()
|
||||
|
||||
const locales = [
|
||||
{ code: 'zh-CN', label: '中' },
|
||||
{ code: 'en', label: 'EN' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lang-switch" role="group" aria-label="language">
|
||||
<button
|
||||
v-for="l in locales"
|
||||
:key="l.code"
|
||||
type="button"
|
||||
class="lang-switch__btn"
|
||||
:class="{ 'is-active': current === l.code }"
|
||||
:disabled="switching"
|
||||
@click="switchLocale(l.code)"
|
||||
>
|
||||
{{ l.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lang-switch {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
}
|
||||
.lang-switch__btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.lang-switch__btn + .lang-switch__btn {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.lang-switch__btn:hover:not(:disabled):not(.is-active) {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.lang-switch__btn.is-active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.lang-switch__btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
}
|
||||
</style>
|
||||
66
src/components/TabNav.vue
Normal file
66
src/components/TabNav.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useProjects } from '../composables/useProjects.js'
|
||||
|
||||
const { projects } = useProjects()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="tab-nav">
|
||||
<div class="container tab-nav__inner">
|
||||
<RouterLink
|
||||
v-for="p in projects"
|
||||
:key="p.id"
|
||||
:to="`/${p.id}`"
|
||||
class="tab-nav__item"
|
||||
active-class="is-active"
|
||||
>
|
||||
{{ $t(p.tabKey) }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-nav {
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.tab-nav__inner {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tab-nav__inner::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.tab-nav__item {
|
||||
position: relative;
|
||||
padding: var(--space-md) var(--space-md);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
.tab-nav__item:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.tab-nav__item.is-active {
|
||||
color: var(--accent);
|
||||
}
|
||||
.tab-nav__item.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>
|
||||
28
src/components/ui/Card.vue
Normal file
28
src/components/ui/Card.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
hoverable: { type: Boolean, default: false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card" :class="{ 'card--hover': hoverable }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
.card--hover {
|
||||
transition: transform var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
.card--hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
</style>
|
||||
25
src/components/ui/Tag.vue
Normal file
25
src/components/ui/Tag.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
tag: { type: String, default: 'span' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="tag" class="tag">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
background: var(--accent-weak);
|
||||
border-radius: 999px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user