<template>
|
<div class="art-full-height flex flex-col gap-5">
|
<section
|
class="overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,var(--art-main-bg-color),var(--art-card-bg-color))] p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)]"
|
>
|
<div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
<div class="max-w-3xl">
|
<div
|
class="mb-3 inline-flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-600"
|
>
|
<span class="size-2 rounded-full bg-emerald-500"></span>
|
RSF Phase 1 Landing
|
</div>
|
<h1
|
class="m-0 text-3xl font-semibold tracking-tight text-[var(--art-gray-900)] md:text-4xl"
|
>
|
运行骨架已经切到 `rsf-design`
|
</h1>
|
<p class="mt-4 max-w-2xl text-sm leading-7 text-[var(--art-gray-600)] md:text-base">
|
当前入口已经接入真实后端登录、动态菜单和权限链路。这个首页只展示已经可用的 phase-1
|
能力,不再保留模板里的示例图表和演示数据。
|
</p>
|
</div>
|
|
<div class="rounded-2xl border border-white/10 bg-white/70 px-4 py-3 backdrop-blur-sm">
|
<p class="text-xs uppercase tracking-[0.24em] text-[var(--art-gray-500)]"
|
>Current Entry</p
|
>
|
<p class="mt-2 text-lg font-semibold text-[var(--art-gray-900)]">
|
{{ currentUserName }}
|
</p>
|
<p class="mt-1 text-sm text-[var(--art-gray-600)]">
|
{{ currentUserRoleText }} · {{ currentMenuLabel }}
|
</p>
|
</div>
|
</div>
|
</section>
|
|
<section class="grid gap-4 md:grid-cols-3">
|
<ArtStatsCard
|
title="已接入后端"
|
:count="backendSwitchCount"
|
description="登录、用户信息、菜单全部来自 rsf-server"
|
icon="ri:server-line"
|
/>
|
<ArtStatsCard
|
title="动态菜单"
|
:count="visibleMenuCount"
|
description="仅发布 phase-1 允许进入的新入口"
|
icon="ri:route-line"
|
/>
|
<ArtStatsCard
|
title="权限链路"
|
:count="permissionSignalCount"
|
description="角色与权限节点已从真实用户数据恢复"
|
icon="ri:shield-check-line"
|
/>
|
</section>
|
|
<section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
<ElCard
|
class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
|
shadow="never"
|
>
|
<template #header>
|
<div class="flex items-center justify-between">
|
<div>
|
<h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">真实运行状态</h2>
|
<p class="mt-1 text-xs text-[var(--art-gray-500)]">
|
这些信息来自当前登录用户和菜单 store
|
</p>
|
</div>
|
<ElTag type="success" effect="light">Backend mode</ElTag>
|
</div>
|
</template>
|
|
<div class="grid gap-4 md:grid-cols-2">
|
<div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
|
<p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">User</p>
|
<p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
|
{{ currentUserName }}
|
</p>
|
<p class="mt-2 text-sm text-[var(--art-gray-600)]">
|
{{ currentUserRoleText }}
|
</p>
|
<div class="mt-4 flex flex-wrap gap-2">
|
<ElTag v-for="role in currentRoles" :key="role" type="info" effect="plain">
|
{{ role }}
|
</ElTag>
|
<ElTag v-if="!currentRoles.length" type="info" effect="plain">No roles</ElTag>
|
</div>
|
</div>
|
|
<div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
|
<p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">Permissions</p>
|
<p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
|
{{ currentAuthorities.length }} auth nodes
|
</p>
|
<p class="mt-2 text-sm text-[var(--art-gray-600)]">
|
权限节点直接来自当前用户的真实 `authorities` 载荷,不再依赖模板演示态。
|
</p>
|
<div class="mt-4 flex flex-wrap gap-2">
|
<ElTag v-for="item in previewAuthorities" :key="item" type="warning" effect="plain">
|
{{ item }}
|
</ElTag>
|
<ElTag v-if="!previewAuthorities.length" type="warning" effect="plain">
|
No authorities
|
</ElTag>
|
</div>
|
</div>
|
</div>
|
</ElCard>
|
|
<ElCard
|
class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
|
shadow="never"
|
>
|
<template #header>
|
<div>
|
<h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">菜单接入清单</h2>
|
<p class="mt-1 text-xs text-[var(--art-gray-500)]">
|
当前菜单树用于验证 `rsf-design` 是否真正接住后端发布
|
</p>
|
</div>
|
</template>
|
|
<div class="space-y-3">
|
<div
|
v-for="item in menuPreview"
|
:key="item.key"
|
class="flex items-center justify-between rounded-2xl border border-[var(--art-gray-200)] px-4 py-3"
|
>
|
<div>
|
<p class="text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
|
<p class="mt-1 text-xs text-[var(--art-gray-500)]">{{ item.description }}</p>
|
</div>
|
<ElTag :type="item.type" effect="light">
|
{{ item.value }}
|
</ElTag>
|
</div>
|
</div>
|
</ElCard>
|
</section>
|
</div>
|
</template>
|
|
<script setup>
|
import { storeToRefs } from 'pinia'
|
import { useUserStore } from '@/store/modules/user'
|
import { useMenuStore } from '@/store/modules/menu'
|
import { formatMenuTitle } from '@/utils/router'
|
|
defineOptions({ name: 'Console' })
|
|
const userStore = useUserStore()
|
const menuStore = useMenuStore()
|
const { getUserInfo } = storeToRefs(userStore)
|
const { menuList } = storeToRefs(menuStore)
|
|
const currentUser = computed(() => getUserInfo.value || {})
|
const currentUserName = computed(() => {
|
return (
|
currentUser.value.userName ||
|
currentUser.value.username ||
|
currentUser.value.nickname ||
|
'RSF User'
|
)
|
})
|
const currentRoles = computed(() => {
|
const roles = currentUser.value.roles
|
if (!Array.isArray(roles)) return []
|
return roles
|
.map((role) => {
|
if (typeof role === 'string') {
|
return role
|
}
|
return role?.code || role?.name || role?.title || ''
|
})
|
.filter(Boolean)
|
})
|
const currentAuthorities = computed(() => {
|
const authorities = currentUser.value.authorities
|
if (!Array.isArray(authorities)) return []
|
return authorities.map((item) => item?.authority || '').filter(Boolean)
|
})
|
const currentMenuLabel = computed(() => {
|
const firstMenu = menuList.value?.[0]
|
return formatMenuTitle(firstMenu?.meta?.title || 'menus.dashboard.console')
|
})
|
const currentUserRoleText = computed(() => {
|
if (!currentRoles.value.length) {
|
return 'No role information yet'
|
}
|
return `Roles: ${currentRoles.value.join(' / ')}`
|
})
|
const backendSwitchCount = computed(() => {
|
return currentUserName.value !== 'RSF User' || menuList.value.length > 0 ? 1 : 0
|
})
|
const visibleMenuCount = computed(() => countVisibleMenus(menuList.value))
|
const permissionSignalCount = computed(() => {
|
return currentRoles.value.length + currentAuthorities.value.length
|
})
|
const previewAuthorities = computed(() => currentAuthorities.value.slice(0, 4))
|
const menuPreview = computed(() => {
|
const total = visibleMenuCount.value
|
const rootCount = Array.isArray(menuList.value) ? menuList.value.length : 0
|
const firstMenu = menuList.value?.[0]
|
const firstChildren = Array.isArray(firstMenu?.children) ? firstMenu.children.length : 0
|
|
return [
|
{
|
key: 'entry',
|
title: '入口模式',
|
description: '当前页面以 backend mode 运行,菜单由服务端驱动',
|
value: 'backend',
|
type: 'success'
|
},
|
{
|
key: 'menus',
|
title: '可见菜单',
|
description: '已通过动态路由适配后进入前端菜单树',
|
value: `${total}`,
|
type: 'primary'
|
},
|
{
|
key: 'root',
|
title: '一级目录',
|
description: '根级菜单节点数量',
|
value: `${rootCount}`,
|
type: 'info'
|
},
|
{
|
key: 'children',
|
title: '首个目录子项',
|
description: '用于快速确认菜单树已被正确展开',
|
value: `${firstChildren}`,
|
type: 'warning'
|
}
|
]
|
})
|
|
function countVisibleMenus(items) {
|
if (!Array.isArray(items)) return 0
|
return items.reduce((total, item) => {
|
const current = item?.meta?.isHide ? 0 : 1
|
return total + current + countVisibleMenus(item?.children)
|
}, 0)
|
}
|
</script>
|