- ty-search-form 折叠默认数 2 → 3 - login/register/forgot/home/ty-header/ty-sidebar/ty-mixbar/ty-tabs/ty-footer/ty-setting-panel/icon/default 布局裸 CSS 转 @apply - 修复背景色工具类误写(bg-base→bg-bg-base、bg-page→bg-bg-page) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
4.3 KiB
Vue
172 lines
4.3 KiB
Vue
<template>
|
||
<div class="home">
|
||
<div class="home__banner">
|
||
<div class="home__banner-decor" aria-hidden="true">
|
||
<span class="home__banner-decor-1"></span>
|
||
<span class="home__banner-decor-2"></span>
|
||
</div>
|
||
<div class="home__banner-inner">
|
||
<h1 class="home__greeting">{{ greeting }},{{ username }}</h1>
|
||
<p class="home__datetime">{{ datetime }}</p>
|
||
<p class="home__subtitle">欢迎使用 {{ title }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="home__modules">
|
||
<el-card v-for="m in topMenus" :key="m.code" class="home__module" shadow="never">
|
||
<div class="home__module-head">
|
||
<icon type="iconify" :icon="m.icon ?? 'mdi:folder'" class="home__module-icon" />
|
||
<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>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useAuthStore } from '@/stores/modules/auth'
|
||
import { useMenuStore } from '@/stores/modules/menu'
|
||
import { defaultLoginConfig } from '@/config/login'
|
||
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const menuStore = useMenuStore()
|
||
|
||
const title = defaultLoginConfig.title
|
||
|
||
const username = computed(() => decodeUsername(auth.accessToken))
|
||
|
||
const greeting = computed(() => {
|
||
const h = new Date().getHours()
|
||
if (h < 6) return '凌晨好'
|
||
if (h < 12) return '早上好'
|
||
if (h < 18) return '下午好'
|
||
return '晚上好'
|
||
})
|
||
|
||
// 实时时间
|
||
const now = ref(new Date())
|
||
const timer = setInterval(() => {
|
||
now.value = new Date()
|
||
}, 1000)
|
||
onBeforeUnmount(() => clearInterval(timer))
|
||
|
||
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>
|
||
|
||
<style scoped lang="scss">
|
||
.home {
|
||
@apply flex flex-col gap-md;
|
||
}
|
||
|
||
.home__banner {
|
||
@apply relative overflow-hidden rounded-lg text-white;
|
||
background: linear-gradient(135deg, var(--wm-color-primary), var(--wm-color-primary-dark-2));
|
||
}
|
||
|
||
.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-90;
|
||
}
|
||
|
||
.home__subtitle {
|
||
@apply mt-md text-sm opacity-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>
|