zhou zhou
22 小时以前 e9283ffe6822b12ec5dd2ccf4dc13a369b227a61
rsf-design/src/views/dashboard/console/index.vue
@@ -1,44 +1,249 @@
<!-- 工作台页面 -->
<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>