| | |
| | | <!-- 工作台页面 --> |
| | | <template> |
| | | <div> |
| | | <CardList></CardList> |
| | | <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> |
| | | |
| | | <ElRow :gutter="20"> |
| | | <ElCol :sm="24" :md="12" :lg="10"> |
| | | <ActiveUser /> |
| | | </ElCol> |
| | | <ElCol :sm="24" :md="12" :lg="14"> |
| | | <SalesOverview /> |
| | | </ElCol> |
| | | </ElRow> |
| | | <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> |
| | | |
| | | <ElRow :gutter="20"> |
| | | <ElCol :sm="24" :md="24" :lg="12"> |
| | | <NewUser /> |
| | | </ElCol> |
| | | <ElCol :sm="24" :md="12" :lg="6"> |
| | | <Dynamic /> |
| | | </ElCol> |
| | | <ElCol :sm="24" :md="12" :lg="6"> |
| | | <TodoList /> |
| | | </ElCol> |
| | | </ElRow> |
| | | <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> |
| | | |
| | | <AboutProject /> |
| | | <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 CardList from './modules/card-list.vue' |
| | | import ActiveUser from './modules/active-user.vue' |
| | | |
| | | import SalesOverview from './modules/sales-overview.vue' |
| | | import NewUser from './modules/new-user.vue' |
| | | |
| | | import Dynamic from './modules/dynamic-stats.vue' |
| | | import TodoList from './modules/todo-list.vue' |
| | | |
| | | import AboutProject from './modules/about-project.vue' |
| | | 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> |