feat(dashboard): 欢迎页对齐原生 GERP 并移除技术栈展示
- 欢迎区:logo + 标题 + 欢迎语 + 每日一言(6s 轮播/点击切换/300ms 淡入淡出)+ 个人中心/快捷操作 - 欢迎区改为占满剩余空间垂直居中(对齐原生 welcome-container),去除面板边框 - 删除「系统信息」面板(含前端/表格/后端技术栈描述行) - 快捷入口超过 10 项折叠为「展开全部(N)/ 收起」,避免菜单过多拉长页面
This commit is contained in:
1 parent
1c0885126d
commit
e79e3bb28b
1 file changed
+207
-26
+207
-26
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { get } from '@/api/request'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@@ -12,6 +12,33 @@ interface DashboardStats {
|
||||
ActiveTenants: number
|
||||
}
|
||||
|
||||
/** 欢迎页 logo(与顶部导航同源 CDN;原生欢迎页用的是 /assets/img/CenterICON.png) */
|
||||
const logoUrl = 'https://hb.cdn.gvsds.com/assets/img/GERP/GERP.png'
|
||||
|
||||
/** 每日一言池(内容沿用原生 GERP 欢迎页 pages/dashboard.html 的 quotes) */
|
||||
const quotes: ReadonlyArray<{ text: string; author: string }> = [
|
||||
{ text: '代码是写给人看的,顺便给机器运行。', author: 'SICP' },
|
||||
{ text: '过早优化是万恶之源。', author: 'Donald Knuth' },
|
||||
{ text: '简单是可靠的前提。', author: 'Dijkstra' },
|
||||
{ text: '不要重复自己。', author: 'DRY 原则' },
|
||||
{ text: '如果你不能简单地解释它,说明你理解得还不够。', author: '爱因斯坦' },
|
||||
{ text: '好代码本身就是最好的文档。', author: 'Steve McConnell' },
|
||||
{ text: '调试比写代码难一倍。', author: 'Brian Kernighan' },
|
||||
{ text: '先让代码能跑,再优化。', author: 'Kent Beck' },
|
||||
{ text: '命名是编程中最难的事情之一。', author: 'Phil Karlton' },
|
||||
{ text: '测试驱动开发不仅仅是一种测试方法。', author: 'Martin Fowler' },
|
||||
{ text: '设计是让东西能用的艺术。', author: 'Steve Jobs' },
|
||||
{ text: '少即是多。', author: 'Mies van der Rohe' },
|
||||
{ text: '任何傻瓜都能写出计算机能理解的代码。', author: 'Martin Fowler' },
|
||||
{ text: '形式追随功能。', author: 'Louis Sullivan' },
|
||||
{ text: '细节决定成败。', author: 'Charles Eames' },
|
||||
{ text: '好设计就是尽可能少的设计。', author: 'Dieter Rams' },
|
||||
{ text: '修改代码不改变行为,这就是重构。', author: 'Martin Fowler' },
|
||||
{ text: '遗留代码就是没有测试的代码。', author: 'Michael Feathers' },
|
||||
{ text: '软件不是写出来的,是改出来的。', author: '佚名' },
|
||||
{ text: '面对接口编程,不是面对实现。', author: 'GoF 设计模式' },
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -23,7 +50,43 @@ const stats = ref<DashboardStats>({
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
// === 每日一言:6s 自动轮播,点击手动切换并重置计时 ===
|
||||
/** 当前名言下标 */
|
||||
const quoteIndex = ref(0)
|
||||
/** 名言透明度状态:切换时先淡出、换文案后淡入(对齐原生 300ms 节奏) */
|
||||
const quoteVisible = ref(true)
|
||||
/** 当前展示的名言(下标越界兜底取首条,避免取值为 undefined) */
|
||||
const currentQuote = computed(() => quotes[quoteIndex.value] ?? quotes[0])
|
||||
|
||||
let quoteTimer: ReturnType<typeof setInterval> | null = null
|
||||
let quoteSwapTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** 切到下一条名言(淡出 → 换文案 → 淡入) */
|
||||
function nextQuote(): void {
|
||||
quoteVisible.value = false
|
||||
if (quoteSwapTimer) clearTimeout(quoteSwapTimer)
|
||||
quoteSwapTimer = setTimeout(() => {
|
||||
quoteIndex.value = (quoteIndex.value + 1) % quotes.length
|
||||
quoteVisible.value = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 点击名言:切换后重新计时,避免刚点完又被自动轮播立刻跳走 */
|
||||
function handleQuoteClick(): void {
|
||||
nextQuote()
|
||||
if (quoteTimer) clearInterval(quoteTimer)
|
||||
quoteTimer = setInterval(nextQuote, 6000)
|
||||
}
|
||||
|
||||
// === 快捷操作按钮:滚动到「快捷入口」面板 ===
|
||||
const quickSectionRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function scrollToQuick(): void {
|
||||
quickSectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
quoteTimer = setInterval(nextQuote, 6000)
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await get<DashboardStats>('/API/Dashboard/Stats')
|
||||
@@ -33,6 +96,11 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (quoteTimer) clearInterval(quoteTimer)
|
||||
if (quoteSwapTimer) clearTimeout(quoteSwapTimer)
|
||||
})
|
||||
|
||||
/** 关键指标(企业控制台 KPI 数值区) */
|
||||
const statItems = computed(() => [
|
||||
{ label: '用户总数', value: String(stats.value.UserCount ?? 0) },
|
||||
@@ -44,6 +112,17 @@ const statItems = computed(() => [
|
||||
/** 快捷入口:取菜单一级项;含子菜单时跳首个子页 */
|
||||
const quickMenus = computed<MenuItem[]>(() => userStore.menus || [])
|
||||
|
||||
/** 快捷入口折叠阈值:超出后只展示前 N 个,其余由「展开全部」收起(避免菜单多时页面被拉长) */
|
||||
const QUICK_LIMIT = 10
|
||||
/** 快捷入口是否展开全部 */
|
||||
const quickExpanded = ref(false)
|
||||
/** 当前实际展示的快捷入口 */
|
||||
const visibleQuickMenus = computed<MenuItem[]>(() =>
|
||||
quickExpanded.value ? quickMenus.value : quickMenus.value.slice(0, QUICK_LIMIT),
|
||||
)
|
||||
/** 是否存在被折叠的快捷入口 */
|
||||
const quickOverflow = computed(() => quickMenus.value.length > QUICK_LIMIT)
|
||||
|
||||
/** 判断图标是否为 FontAwesome 类名(与布局菜单一致) */
|
||||
const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
if (!icon) return false
|
||||
@@ -56,16 +135,33 @@ function goMenu(menu: MenuItem): void {
|
||||
const target = menu.Path || menu.Children?.[0]?.Path
|
||||
if (target) router.push(target)
|
||||
}
|
||||
|
||||
/** 当前角色文案 */
|
||||
const roleLabel = computed(() => {
|
||||
if (userStore.isAdmin) return '管理员'
|
||||
return userStore.currentRole || '普通用户'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="dashboard">
|
||||
<!-- 欢迎区(对齐原生 GERP 欢迎页:logo + 标题 + 每日一言 + 快捷操作) -->
|
||||
<section class="welcome-panel">
|
||||
<img class="welcome-logo" :src="logoUrl" alt="GERP" />
|
||||
<h1 class="welcome-title">GERP 管理系统</h1>
|
||||
<p class="welcome-subtitle">{{ userStore.displayName }},欢迎回来</p>
|
||||
<div class="welcome-quote" title="点击换一条" @click="handleQuoteClick">
|
||||
<div class="quote-text" :class="quoteVisible ? 'quote-fade-in' : 'quote-fade-out'">
|
||||
“{{ currentQuote.text }}”
|
||||
</div>
|
||||
<div class="quote-author" :class="quoteVisible ? 'quote-fade-in' : 'quote-fade-out'">
|
||||
— {{ currentQuote.author }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<el-button type="primary" @click="router.push('/profile')">
|
||||
<el-icon><User /></el-icon>个人中心
|
||||
</el-button>
|
||||
<el-button @click="scrollToQuick">
|
||||
<el-icon><Grid /></el-icon>快捷操作
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 关键指标 -->
|
||||
<section class="gerp-panel stat-panel">
|
||||
<div class="gerp-panel-header">
|
||||
@@ -81,14 +177,14 @@ const roleLabel = computed(() => {
|
||||
</section>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<section v-if="quickMenus.length" class="gerp-panel">
|
||||
<section v-if="quickMenus.length" ref="quickSectionRef" class="gerp-panel">
|
||||
<div class="gerp-panel-header">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>快捷入口</span>
|
||||
</div>
|
||||
<div class="quick-grid">
|
||||
<div
|
||||
v-for="menu in quickMenus"
|
||||
v-for="menu in visibleQuickMenus"
|
||||
:key="menu.Path || menu.Title"
|
||||
class="quick-item"
|
||||
@click="goMenu(menu)"
|
||||
@@ -97,24 +193,13 @@ const roleLabel = computed(() => {
|
||||
<el-icon v-else-if="menu.Icon" class="quick-item-icon"><component :is="menu.Icon" /></el-icon>
|
||||
<span class="quick-item-label">{{ menu.Title }}</span>
|
||||
</div>
|
||||
<!-- 折叠开关:仅在菜单超出阈值时出现,展开/收起不改变面板宽度 -->
|
||||
<div v-if="quickOverflow" class="quick-item quick-item-toggle" @click="quickExpanded = !quickExpanded">
|
||||
<el-icon class="quick-item-icon"><component :is="quickExpanded ? 'ArrowUp' : 'MoreFilled'" /></el-icon>
|
||||
<span class="quick-item-label">{{ quickExpanded ? '收起' : `展开全部(${quickMenus.length})` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<section class="gerp-panel">
|
||||
<div class="gerp-panel-header">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>系统信息</span>
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="当前用户">{{ userStore.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前角色">{{ roleLabel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="前端技术栈">Vue 3 + TypeScript + Element Plus</el-descriptions-item>
|
||||
<el-descriptions-item label="数据表格">vxe-table · FormKit 表单</el-descriptions-item>
|
||||
<el-descriptions-item label="后端服务">Django + DRF</el-descriptions-item>
|
||||
<el-descriptions-item label="系统版本">v1.0.0</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,6 +214,79 @@ const roleLabel = computed(() => {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 欢迎区:不带面板边框;占用剩余空间并垂直居中(原生欢迎页即整屏居中,而非贴顶) */
|
||||
.welcome-panel {
|
||||
flex: 1 1 auto;
|
||||
/* 保底高度:面板内容较高时仍保证欢迎区有足够留白,不被压扁 */
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.welcome-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 12px 0 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 每日一言:可点击换一条,切换走透明度过渡 */
|
||||
.welcome-quote {
|
||||
margin-top: 18px;
|
||||
max-width: 640px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.quote-text,
|
||||
.quote-author {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.quote-author {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.quote-fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.quote-fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 关键指标:KPI 数值区(无卡片、无装饰渐变,靠分隔线分区) */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
@@ -138,6 +296,11 @@ const roleLabel = computed(() => {
|
||||
.stat-cell {
|
||||
padding: 4px 20px;
|
||||
|
||||
/* 首个单元去掉左内边距,与面板标题左对齐(其余靠 20px 内边距形成分区留白) */
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
& + .stat-cell {
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
@@ -157,7 +320,11 @@ const roleLabel = computed(() => {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 快捷入口:图标宫格 */
|
||||
/* 快捷入口:图标宫格(面板与欢迎区共用纵向空间,禁止被压缩) */
|
||||
.dashboard > .gerp-panel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||
@@ -196,4 +363,18 @@ const roleLabel = computed(() => {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 折叠开关:虚线描边与彩色图标区分"可展开",不参与跳转 */
|
||||
.quick-item-toggle {
|
||||
border-color: var(--el-border-color);
|
||||
border-style: dashed;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.quick-item-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user