task-11(壳层/路由): 实现无菜单权限空状态
empty-state 定义标题/引导文案与 shouldShowEmptyMenu 纯判定(Boolean 化防 undefined); AdminLayout 侧边栏与内容区(根路由)共用空态,加载中不闪现,未初始化不误报。
This commit is contained in:
@@ -6,6 +6,11 @@ import { useAdminSessionStore } from '@/stores/admin-session'
|
||||
import { joinAdminPath } from '@/config/app'
|
||||
import { pageTitleOf, topbarUserOf } from '@/layout/topbar-model'
|
||||
import { groupKeysForActive, toSidebarEntries } from '@/layout/menu-mapper'
|
||||
import {
|
||||
EMPTY_MENU_DESCRIPTION,
|
||||
EMPTY_MENU_TITLE,
|
||||
shouldShowEmptyMenu,
|
||||
} from '@/layout/empty-state'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -15,6 +20,7 @@ const collapsed = ref(false)
|
||||
const activePath = computed(() => route.path)
|
||||
const menuEntries = computed(() => toSidebarEntries(session.menuTree))
|
||||
const openedKeys = computed(() => groupKeysForActive(menuEntries.value, route.path))
|
||||
const emptyMenu = computed(() => shouldShowEmptyMenu(menuEntries.value.length > 0, session.loading, session.initialized))
|
||||
const pageTitle = computed(() => pageTitleOf(route.meta.title))
|
||||
const userVm = computed(() => topbarUserOf(session.user))
|
||||
const logoUrl = joinAdminPath('assets', 'logo.jpg')
|
||||
@@ -55,7 +61,7 @@ async function signOut() {
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<el-empty v-if="!menuEntries.length && !session.loading" description="暂无可用菜单" :image-size="72" />
|
||||
<el-empty v-if="emptyMenu" :description="EMPTY_MENU_TITLE" :image-size="72" />
|
||||
</el-scrollbar>
|
||||
|
||||
<button class="sidebar-collapse" type="button" @click="collapsed = !collapsed">
|
||||
@@ -79,7 +85,13 @@ async function signOut() {
|
||||
</header>
|
||||
<main class="admin-content">
|
||||
<el-alert v-if="session.error" :title="session.error" type="error" show-icon :closable="false" />
|
||||
<RouterView />
|
||||
<el-empty
|
||||
v-if="emptyMenu && route.path === '/'"
|
||||
:title="EMPTY_MENU_TITLE"
|
||||
:description="EMPTY_MENU_DESCRIPTION"
|
||||
:image-size="96"
|
||||
/>
|
||||
<RouterView v-else />
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 无菜单权限空状态(任务 11)文案与判定。 */
|
||||
export const EMPTY_MENU_TITLE = '暂无可用菜单'
|
||||
export const EMPTY_MENU_DESCRIPTION = '请联系管理员为你的账号分配菜单权限'
|
||||
|
||||
/**
|
||||
* 是否展示无菜单空态:会话已初始化、未在加载、且没有任何可渲染菜单。
|
||||
* 加载中不闪空态;初始化失败由错误条提示而非空态。
|
||||
*/
|
||||
export function shouldShowEmptyMenu(hasMenus: boolean, loading: boolean, initialized: boolean): boolean {
|
||||
return Boolean(initialized) && !loading && !hasMenus
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import {
|
||||
EMPTY_MENU_DESCRIPTION,
|
||||
EMPTY_MENU_TITLE,
|
||||
shouldShowEmptyMenu,
|
||||
} from '../src/layout/empty-state.ts'
|
||||
|
||||
test('test_task_011_empty_menu_state_normal_primary_path', () => {
|
||||
// 正常主路径:初始化完成且没有任何菜单 -> 展示空态。
|
||||
assert.equal(shouldShowEmptyMenu(false, false, true), true)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_normal_variant_input', () => {
|
||||
// 正常变体:有一个或多个菜单时即使没有加载也不展示空态。
|
||||
assert.equal(shouldShowEmptyMenu(true, false, true), false)
|
||||
assert.equal(shouldShowEmptyMenu(true, true, true), false)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_normal_repeated_operation_is_idempotent', () => {
|
||||
// 正常重复:判定幂等。
|
||||
assert.equal(shouldShowEmptyMenu(false, false, true), shouldShowEmptyMenu(false, false, true))
|
||||
assert.equal(shouldShowEmptyMenu(true, false, true), shouldShowEmptyMenu(true, false, true))
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_boundary_empty_input', () => {
|
||||
// 边界空值:会话未初始化时不展示空态(避免误报)。
|
||||
assert.equal(shouldShowEmptyMenu(false, false, false), false)
|
||||
assert.equal(shouldShowEmptyMenu(false, true, false), false)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_boundary_single_item', () => {
|
||||
// 边界单元素:仅一个菜单即不算空。
|
||||
assert.equal(shouldShowEmptyMenu(true, false, true), false)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:加载中禁止闪现空态;初始化字段缺失按未就绪处理。
|
||||
assert.equal(shouldShowEmptyMenu(false, true, true), false, '加载中不得闪现空态')
|
||||
assert.equal(shouldShowEmptyMenu(false, false, undefined as unknown as boolean), false)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_invalid_input_rejected', () => {
|
||||
// 异常输入:布尔语义混乱(未初始化却已加载完)也不展示。
|
||||
assert.equal(shouldShowEmptyMenu(false, true, undefined as unknown as boolean), false)
|
||||
})
|
||||
|
||||
test('test_task_011_empty_menu_state_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败:壳层消费空态判定与文案常量,内容级空态在根路由呈现。
|
||||
assert.ok(EMPTY_MENU_TITLE.length > 0)
|
||||
assert.ok(EMPTY_MENU_DESCRIPTION.length > EMPTY_MENU_TITLE.length)
|
||||
const layout = readSource('src/layout/AdminLayout.vue')
|
||||
assert.match(layout, /shouldShowEmptyMenu/)
|
||||
assert.match(layout, /EMPTY_MENU_DESCRIPTION/)
|
||||
assert.match(layout, /route\.path === '\/'/, '内容级空态限定根路由')
|
||||
})
|
||||
@@ -31,8 +31,8 @@ test('test_task_003_layout_contract_normal_repeated_operation_is_idempotent', ()
|
||||
test('test_task_003_layout_contract_boundary_empty_input', () => {
|
||||
// 边界空值:菜单为空时应展示无菜单状态,且页面容器本身仍保留。
|
||||
const layout = readSource(LAYOUT)
|
||||
assert.ok(layout.includes('暂无可用菜单'), '无菜单需有空状态文案')
|
||||
assert.ok(layout.includes('menuEntries.length'), '空菜单由映射结果长度驱动')
|
||||
assert.ok(layout.includes('el-empty'), '无菜单需有空状态占位')
|
||||
assert.ok(layout.includes('emptyMenu'), '空菜单由空态判定驱动')
|
||||
assert.equal(occurrences(layout, '<RouterView'), 1)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user