refactor: 精简导航并扩大工作区内容显示面积
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { NScrollbar } from 'naive-ui'
|
||||
import { provide, ref } from 'vue'
|
||||
import { workspaceHostKey } from './workspaceContext'
|
||||
/** 工作区固定标题和操作区;仅内容容器滚动,不向 document 传播滚动。 */
|
||||
defineProps<{ split?: boolean }>()
|
||||
defineProps<{ split?: boolean; compact?: boolean }>()
|
||||
const host = ref<HTMLElement | null>(null)
|
||||
provide(workspaceHostKey, host)
|
||||
</script>
|
||||
<template>
|
||||
<section class="workspace-page">
|
||||
<section ref="host" class="workspace-page" :class="{ 'content-first-workspace': compact }">
|
||||
<NScrollbar v-if="$slots.header" class="workspace-heading-scroll"
|
||||
><header class="workspace-heading"><slot name="header" /></header
|
||||
></NScrollbar>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { NButton, NDrawer, NDrawerContent } from 'naive-ui'
|
||||
import { Settings2, X } from '@lucide/vue'
|
||||
import { workspaceHostKey } from './workspaceContext'
|
||||
|
||||
/** 低频配置、批量动作与回执在抽屉中查看,开关面板不影响页面查询和后台任务。 */
|
||||
withDefaults(defineProps<{ title: string; label?: string; hasReceipt?: boolean }>(), { label: '操作与回执' })
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const host = inject(workspaceHostKey, undefined)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NButton
|
||||
class="workspace-tools-trigger"
|
||||
data-workspace-tools
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="open"
|
||||
@click="open = true"
|
||||
>
|
||||
<template #icon><Settings2 :size="15" /></template>
|
||||
{{ label }}<span v-if="hasReceipt" class="text-xs text-muted"> · 有回执</span>
|
||||
</NButton>
|
||||
<NDrawer
|
||||
v-model:show="open"
|
||||
:to="host ?? undefined"
|
||||
width="min(960px, 100%)"
|
||||
placement="right"
|
||||
:block-scroll="false"
|
||||
display-directive="show"
|
||||
class="workspace-tools-drawer"
|
||||
:aria-label="title"
|
||||
>
|
||||
<NDrawerContent :native-scrollbar="false" body-content-class="workspace-tools-body">
|
||||
<template #header
|
||||
><div class="workspace-tools-heading">
|
||||
<span>{{ title }}</span
|
||||
><NButton quaternary circle aria-label="关闭操作面板" @click="open = false"
|
||||
><X :size="18"
|
||||
/></NButton></div
|
||||
></template>
|
||||
<slot />
|
||||
<template #footer><span class="text-xs text-muted">关闭面板不会取消已提交的后台任务。</span></template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
@@ -4,3 +4,4 @@ export { default as EmptyState } from './EmptyState.vue'
|
||||
export { default as StatusBadge } from './StatusBadge.vue'
|
||||
export { default as AssetImage } from './AssetImage.vue'
|
||||
export { default as DirectoryItem } from './DirectoryItem.vue'
|
||||
export { default as WorkspaceTools } from './WorkspaceTools.vue'
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { h } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { NConfigProvider, NModal, NScrollbar } from 'naive-ui'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NConfigProvider, NDrawer, NDrawerContent, NModal, NScrollbar } from 'naive-ui'
|
||||
import App from '../../App.vue'
|
||||
import WorkspacePage from './WorkspacePage.vue'
|
||||
import AppDialog from './AppDialog.vue'
|
||||
import WorkspaceTools from './WorkspaceTools.vue'
|
||||
import ProjectLayout from '../../features/projects/ProjectLayout.vue'
|
||||
import ConfirmAction from '../../features/workflows/ConfirmAction.vue'
|
||||
import { projectsApi } from '../../features/projects/api'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
let wrapper: VueWrapper | undefined
|
||||
@@ -14,6 +18,7 @@ afterEach(() => {
|
||||
wrapper = undefined
|
||||
localStorage.clear()
|
||||
document.body.innerHTML = ''
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('管理后台组件边界', () => {
|
||||
@@ -93,4 +98,101 @@ describe('管理后台组件边界', () => {
|
||||
})
|
||||
expect(document.querySelector('.dialog-body-scroll .n-scrollbar-container')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('常用工具固定在列表外,批量抽屉挂载于工作区,开关保留列表与配置草稿', async () => {
|
||||
wrapper = mount(WorkspacePage, {
|
||||
attachTo: document.body,
|
||||
props: { split: true, compact: true },
|
||||
slots: {
|
||||
header: () => h(WorkspaceTools, { title: '测试操作' }, () => h('input', { 'aria-label': '配置草稿' })),
|
||||
default: '<article class="test-list">主体列表</article>'
|
||||
}
|
||||
})
|
||||
const list = wrapper.get('.test-list').element
|
||||
expect(wrapper.find('.workspace-tools-drawer').exists()).toBe(false)
|
||||
expect(wrapper.get('.workspace-heading').find('[data-workspace-tools]').exists()).toBe(true)
|
||||
await wrapper.get('[data-workspace-tools]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.getComponent(NDrawer).props('to')).toBe(wrapper.get('.workspace-page').element)
|
||||
expect(wrapper.getComponent(NDrawerContent).props('nativeScrollbar')).toBe(false)
|
||||
expect(wrapper.get('.workspace-tools-drawer').element.closest('.workspace-split')).toBeNull()
|
||||
expect(wrapper.find('.workspace-tools-drawer .n-scrollbar-container').exists()).toBe(true)
|
||||
await wrapper.get('input[aria-label="配置草稿"]').setValue('保留配置')
|
||||
await wrapper.get('[aria-label="关闭操作面板"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.getComponent(NDrawer).props('show')).toBe(false)
|
||||
expect(wrapper.get('.test-list').element).toBe(list)
|
||||
await wrapper.get('[data-workspace-tools]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.get<HTMLInputElement>('input[aria-label="配置草稿"]').element.value).toBe('保留配置')
|
||||
})
|
||||
|
||||
it('移除内容区重复流程导航后,七条左侧链接、项目标题与刷新仍可用', async () => {
|
||||
const detail = vi.spyOn(projectsApi, 'detail').mockResolvedValue({
|
||||
id: 'navigation-test',
|
||||
title: '导航测试项目',
|
||||
topic: '测试主题',
|
||||
style: null,
|
||||
status: 'completed',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
episodes: [],
|
||||
characters: [],
|
||||
world: null,
|
||||
reviews: [],
|
||||
tasks: []
|
||||
})
|
||||
vi.spyOn(projectsApi, 'checkpoints').mockResolvedValue([])
|
||||
const paths = [
|
||||
'create-drama',
|
||||
'breakdown',
|
||||
'visual-style',
|
||||
'subject-identity',
|
||||
'subject-images',
|
||||
'storyboard',
|
||||
'production'
|
||||
]
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/projects/:projectId',
|
||||
component: ProjectLayout,
|
||||
children: paths.map(path => ({
|
||||
path,
|
||||
component: { render: () => h(WorkspacePage, {}, () => path) }
|
||||
}))
|
||||
}
|
||||
]
|
||||
})
|
||||
await router.push('/projects/navigation-test/create-drama')
|
||||
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[aria-label="项目工作流"]').exists()).toBe(false)
|
||||
expect(wrapper.get('.project-title').text()).toBe('导航测试项目')
|
||||
for (const path of paths) {
|
||||
await wrapper.get(`.n-menu a[href="/projects/navigation-test/${path}"]`).trigger('click')
|
||||
await flushPromises()
|
||||
expect(router.currentRoute.value.path).toBe('/projects/navigation-test/' + path)
|
||||
expect(wrapper.get('.project-view .workspace-page').text()).toBe(path)
|
||||
}
|
||||
expect(detail).toHaveBeenCalledOnce()
|
||||
await wrapper.get('.project-header .n-button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(detail).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('确认按钮有稳定根元素承接调用方间距,生产按钮显式保留上下留白', () => {
|
||||
wrapper = mount(ConfirmAction, {
|
||||
props: { label: '测试操作', description: '仅展示,不提交请求' },
|
||||
attrs: { class: 'mt-4' }
|
||||
})
|
||||
expect(wrapper.element.tagName).toBe('SPAN')
|
||||
expect(wrapper.classes()).toContain('confirm-action')
|
||||
expect(wrapper.classes()).toContain('mt-4')
|
||||
expect(wrapper.get('button').text()).toBe('测试操作')
|
||||
expect(readFileSync('src/admin.css', 'utf8')).toMatch(
|
||||
/\.production-pipeline-card > \.confirm-action\s*\{[^}]*margin-block:\s*16px 4px/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
/** 操作抽屉挂载到当前工作区,覆盖内容而不挤压列表或遮住整个应用导航。 */
|
||||
export const workspaceHostKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('workspace-host')
|
||||
Reference in New Issue
Block a user