feat: home welcome page

- 登录后首页改为欢迎页:问候语 + 用户名 + 实时时间 + 菜单驱动的快捷入口卡片

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 13:03:42 +08:00
parent 7d0088dd10
commit 8742932f21
2 changed files with 163 additions and 50 deletions

View File

@ -1,61 +1,174 @@
<!--
@name: index.vue
@description: 首页
@author: 程序员优雅哥 youyacoder
@time: 2026/2/11 15:58
-->
<template> <template>
<div class="flex gap-2"> <div class="home">
<button <div class="home__banner">
v-for="lang in languages" <div class="home__banner-decor" aria-hidden="true">
:key="lang.value" <span class="home__banner-decor-1"></span>
:class="{ active: currentLocale === lang.value }" <span class="home__banner-decor-2"></span>
@click="onChangeLanguage(lang.value)" </div>
> <div class="home__banner-inner">
{{ lang.label }} <h1 class="home__greeting">{{ greeting }},{{ username }}</h1>
</button> <p class="home__datetime">{{ datetime }}</p>
</div> <p class="home__subtitle">欢迎使用 {{ title }}</p>
<div>{{ $t('common.welcome', { name: '优雅哥' }) }}</div> </div>
<div>{{ $t('home.title') }}</div> </div>
<hr />
<h1 class="index">index page</h1> <div class="home__modules">
<div>{{ appName }}</div> <el-card v-for="m in topMenus" :key="m.code" class="home__module" shadow="never">
<div>{{ msg }}</div> <div class="home__module-head">
<div>isDark: {{ isDark }}</div> <icon type="iconify" :icon="m.icon ?? 'mdi:folder'" class="home__module-icon" />
<button @click="toggleDark()">Toggle</button> <span class="home__module-title">{{ m.name }}</span>
</div>
<div class="home__module-links">
<el-link
v-for="c in m.children"
:key="c.code"
class="home__module-link"
:underline="false"
@click="router.push(c.path ?? c.code)"
>
{{ c.name }}
</el-link>
</div>
</el-card>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { computed, onBeforeUnmount, ref } from 'vue'
import { loadLanguage } from '@/i18n' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/modules/auth'
import { useMenuStore } from '@/stores/modules/menu'
import { defaultLoginConfig } from '@/config/login'
const { t, locale } = useI18n() const router = useRouter()
console.log(t('common.welcome', { name: '优雅哥' })) const auth = useAuthStore()
const currentLocale = locale const menuStore = useMenuStore()
// 支持的语言
const languages = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
]
const onChangeLanguage = async (lang: string) => {
// 加载语言包
await loadLanguage(lang)
// 设置当前语言
locale.value = lang
}
const msg = ref('Hello world') const title = defaultLoginConfig.title
const isDark = useDark({ const username = computed(() => decodeUsername(auth.accessToken))
selector: 'html',
attribute: 'data-theme', const greeting = computed(() => {
valueDark: 'dark', const h = new Date().getHours()
valueLight: 'light', if (h < 6) return '凌晨好'
if (h < 12) return '早上好'
if (h < 18) return '下午好'
return '晚上好'
}) })
const toggleDark = useToggle(isDark) // 实时时间
const now = ref(new Date())
const timer = setInterval(() => {
now.value = new Date()
}, 1000)
onBeforeUnmount(() => clearInterval(timer))
const appName = import.meta.env?.VITE_APP_NAME ?? '' const datetime = computed(() => {
const d = now.value
const pad = (n: number) => String(n).padStart(2, '0')
const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 星期${week} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
})
/** 顶级菜单(有子菜单的,作为快捷入口)。 */
const topMenus = computed(() => menuStore.menus.filter((m) => m.children?.length))
function decodeUsername(token: string | null): string {
if (!token) return ''
try {
const payload = token.split('.')[1]
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=')
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
const json = JSON.parse(new TextDecoder().decode(bytes))
return (json.username as string) ?? ''
} catch {
return ''
}
}
</script> </script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.home {
@apply flex flex-col gap-md;
}
.home__banner {
@apply relative overflow-hidden rounded-lg;
background: linear-gradient(135deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
color: #fff;
}
.home__banner-decor {
@apply absolute inset-0;
}
.home__banner-decor-1 {
@apply absolute rounded-full;
width: 260px;
height: 260px;
top: -100px;
right: -60px;
background: rgba(255, 255, 255, 0.08);
}
.home__banner-decor-2 {
@apply absolute rounded-full;
width: 160px;
height: 160px;
bottom: -60px;
right: 120px;
background: rgba(255, 255, 255, 0.05);
}
.home__banner-inner {
@apply relative px-xl py-2xl;
}
.home__greeting {
@apply text-2xl font-semibold;
}
.home__datetime {
@apply mt-sm text-base;
opacity: 0.9;
}
.home__subtitle {
@apply mt-md text-sm;
opacity: 0.75;
}
.home__modules {
@apply grid grid-cols-1 gap-md md:grid-cols-2 xl:grid-cols-3;
}
.home__module {
@apply transition-shadow hover:shadow-md;
:deep(.el-card__body) {
@apply p-lg;
}
}
.home__module-head {
@apply flex items-center gap-sm;
}
.home__module-icon {
@apply text-2xl;
color: var(--wm-color-primary);
}
.home__module-title {
@apply text-base font-medium text-text-primary;
}
.home__module-links {
@apply mt-md flex flex-wrap gap-x-lg gap-y-sm;
}
.home__module-link {
@apply text-sm text-text-regular;
}
</style>

File diff suppressed because one or more lines are too long