task-16(壳层/路由): 实现退出登录入口的壳层交互(确认式)
logout.ts 定义 runLogout 纯流程(确认通过才 signOut,异常上抛由壳层反馈); AdminLayout 用 ElMessageBox.confirm 确认后再执行 store.signOut($reset + /login)。
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useAdminSessionStore } from '@/stores/admin-session'
|
import { useAdminSessionStore } from '@/stores/admin-session'
|
||||||
import { joinAdminPath } from '@/config/app'
|
import { joinAdminPath } from '@/config/app'
|
||||||
import { pageTitleOf, topbarUserOf } from '@/layout/topbar-model'
|
import { pageTitleOf, topbarUserOf } from '@/layout/topbar-model'
|
||||||
@@ -14,6 +14,13 @@ import {
|
|||||||
import GlobalErrorContainer from '@/layout/GlobalErrorContainer.vue'
|
import GlobalErrorContainer from '@/layout/GlobalErrorContainer.vue'
|
||||||
import { initialCollapsed, toggleCollapsed, writeCollapsePreference } from '@/layout/sidebar-collapse'
|
import { initialCollapsed, toggleCollapsed, writeCollapsePreference } from '@/layout/sidebar-collapse'
|
||||||
import { crumbsForActiveRoute, resolveDocumentTitle, updateDocumentTitle } from '@/layout/title-breadcrumb'
|
import { crumbsForActiveRoute, resolveDocumentTitle, updateDocumentTitle } from '@/layout/title-breadcrumb'
|
||||||
|
import {
|
||||||
|
LOGOUT_CONFIRM_CANCEL,
|
||||||
|
LOGOUT_CONFIRM_MESSAGE,
|
||||||
|
LOGOUT_CONFIRM_OK,
|
||||||
|
LOGOUT_CONFIRM_TITLE,
|
||||||
|
runLogout,
|
||||||
|
} from '@/layout/logout'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -52,7 +59,18 @@ watch(
|
|||||||
|
|
||||||
async function signOut() {
|
async function signOut() {
|
||||||
try {
|
try {
|
||||||
await session.signOut()
|
await runLogout({
|
||||||
|
confirm: () =>
|
||||||
|
ElMessageBox.confirm(LOGOUT_CONFIRM_MESSAGE, LOGOUT_CONFIRM_TITLE, {
|
||||||
|
confirmButtonText: LOGOUT_CONFIRM_OK,
|
||||||
|
cancelButtonText: LOGOUT_CONFIRM_CANCEL,
|
||||||
|
type: 'warning',
|
||||||
|
}).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
signOut: () => session.signOut(),
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '退出登录失败')
|
ElMessage.error(error instanceof Error ? error.message : '退出登录失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** 壳层退出登录交互(任务 16):确认后执行,取消/拒绝一律不退出。 */
|
||||||
|
|
||||||
|
export const LOGOUT_CONFIRM_TITLE = '退出登录'
|
||||||
|
export const LOGOUT_CONFIRM_MESSAGE = '确定要退出当前账号吗?'
|
||||||
|
export const LOGOUT_CONFIRM_OK = '退出'
|
||||||
|
export const LOGOUT_CONFIRM_CANCEL = '取消'
|
||||||
|
|
||||||
|
export interface LogoutDeps {
|
||||||
|
/** 用户确认对话框,resolve true 表示确认退出。 */
|
||||||
|
confirm: () => Promise<boolean>
|
||||||
|
/** 真正执行退出(清状态 + 跳登录页)。 */
|
||||||
|
signOut: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出流程:先确认;确认后执行退出;signOut/confirm 异常向上抛由调用方反馈。 */
|
||||||
|
export async function runLogout({ confirm, signOut }: LogoutDeps): Promise<void> {
|
||||||
|
const proceed = await confirm()
|
||||||
|
if (!proceed) return
|
||||||
|
await signOut()
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readSource } from './helpers.ts'
|
||||||
|
import {
|
||||||
|
LOGOUT_CONFIRM_CANCEL,
|
||||||
|
LOGOUT_CONFIRM_MESSAGE,
|
||||||
|
LOGOUT_CONFIRM_OK,
|
||||||
|
LOGOUT_CONFIRM_TITLE,
|
||||||
|
runLogout,
|
||||||
|
} from '../src/layout/logout.ts'
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_normal_primary_path', async () => {
|
||||||
|
// 正常主路径:确认后执行退出。
|
||||||
|
let signedOut = 0
|
||||||
|
await runLogout({ confirm: async () => true, signOut: async () => { signedOut += 1 } })
|
||||||
|
assert.equal(signedOut, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_normal_variant_input', async () => {
|
||||||
|
// 正常变体:取消确认则不退出。
|
||||||
|
let signedOut = 0
|
||||||
|
await runLogout({ confirm: async () => false, signOut: async () => { signedOut += 1 } })
|
||||||
|
assert.equal(signedOut, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_normal_repeated_operation_is_idempotent', async () => {
|
||||||
|
// 正常重复:多次确认分别各执行一次,互不污染状态。
|
||||||
|
let signedOut = 0
|
||||||
|
const deps = { confirm: async () => true, signOut: async () => { signedOut += 1 } }
|
||||||
|
await runLogout(deps)
|
||||||
|
await runLogout(deps)
|
||||||
|
assert.equal(signedOut, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_boundary_empty_input', async () => {
|
||||||
|
// 边界空值:确认被拒后即便 signOut 有副作用也不会被调用。
|
||||||
|
let signedOut = 0
|
||||||
|
await runLogout({ confirm: async () => false, signOut: async () => { signedOut += 1 } })
|
||||||
|
assert.equal(signedOut, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_boundary_single_item', async () => {
|
||||||
|
// 边界单元素:signOut 异常向上抛出,供调用方反馈(不静默)。
|
||||||
|
await assert.rejects(
|
||||||
|
runLogout({ confirm: async () => true, signOut: async () => { throw new Error('退出接口失败') } }),
|
||||||
|
/退出接口失败/,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_boundary_limit_or_missing_field', () => {
|
||||||
|
// 边界上限/缺字段:确认文案齐全、按钮语义完整。
|
||||||
|
assert.ok(LOGOUT_CONFIRM_MESSAGE.length > 0)
|
||||||
|
assert.ok(LOGOUT_CONFIRM_TITLE.length > 0)
|
||||||
|
assert.ok(LOGOUT_CONFIRM_OK.length > 0)
|
||||||
|
assert.ok(LOGOUT_CONFIRM_CANCEL.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_invalid_input_rejected', async () => {
|
||||||
|
// 异常输入:确认对话框异常(fail-closed),不得退出。
|
||||||
|
let signedOut = 0
|
||||||
|
await assert.rejects(
|
||||||
|
runLogout({
|
||||||
|
confirm: async () => { throw new Error('弹窗异常') },
|
||||||
|
signOut: async () => { signedOut += 1 },
|
||||||
|
}),
|
||||||
|
/弹窗异常/,
|
||||||
|
)
|
||||||
|
assert.equal(signedOut, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_016_shell_logout_interaction_dependency_failure_returns_actionable_message', () => {
|
||||||
|
// 依赖失败:壳层使用 ElMessageBox 确认 + runLogout;store 退出清态并跳登录。
|
||||||
|
const layout = readSource('src/layout/AdminLayout.vue')
|
||||||
|
assert.match(layout, /ElMessageBox\.confirm/)
|
||||||
|
assert.match(layout, /runLogout/)
|
||||||
|
const store = readSource('src/stores/admin-session.ts')
|
||||||
|
assert.match(store, /\$reset\(\)/)
|
||||||
|
assert.match(store, /location\.assign\('\/login'\)/)
|
||||||
|
assert.match(layout, /LOGOUT_CONFIRM_MESSAGE/)
|
||||||
|
})
|
||||||
@@ -47,7 +47,7 @@ test('test_task_003_layout_contract_boundary_single_item', () => {
|
|||||||
test('test_task_003_layout_contract_boundary_limit_or_missing_field', () => {
|
test('test_task_003_layout_contract_boundary_limit_or_missing_field', () => {
|
||||||
// 边界上限:Element Plus 交互能力按需命名导入,不整包拖入壳层。
|
// 边界上限:Element Plus 交互能力按需命名导入,不整包拖入壳层。
|
||||||
const layout = readSource(LAYOUT)
|
const layout = readSource(LAYOUT)
|
||||||
assert.match(layout, /import\s*\{\s*ElMessage\s*\}\s*from\s*['"]element-plus['"]/)
|
assert.match(layout, /import\s*\{\s*ElMessage(?:,\s*\w+)*\s*\}\s*from\s*['"]element-plus['"]/)
|
||||||
assert.equal(occurrences(layout, 'import ElementPlus'), 0, '壳层不得整包导入 ElementPlus')
|
assert.equal(occurrences(layout, 'import ElementPlus'), 0, '壳层不得整包导入 ElementPlus')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user