fix(desktop): 补齐顶部段落菜单与浮动按钮布局
This commit is contained in:
@@ -6,6 +6,7 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { isDesktop } from '@/services/platform/desktop'
|
||||
import { minimizeWindow, requestWindowClose, toggleMaximizeWindow } from '@/services/platform/windowControls'
|
||||
@@ -61,6 +62,7 @@ function toggleFromTitlebar(event: MouseEvent) {
|
||||
{{ currentFileName }}
|
||||
<span v-if="isDirty" class="dirty-dot" />
|
||||
</span>
|
||||
<TitleBarMenu />
|
||||
</div>
|
||||
<div class="titlebar-center" data-tauri-drag-region>
|
||||
<span class="app-name" data-tauri-drag-region>NotesAgent</span>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
|
||||
const execute = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/services/editorCommandService', () => ({ executeEditorCommand: execute }))
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
execute.mockReset().mockResolvedValue({ ok: true })
|
||||
})
|
||||
|
||||
describe('桌面顶部段落菜单', () => {
|
||||
it('源码笔记通过统一编辑命令执行属性导入', async () => {
|
||||
const editor = useEditorStore()
|
||||
editor.mode = 'source'
|
||||
editor.currentFilePath = '/示例.md'
|
||||
editor.saveStatus = 'saved'
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('.menu-trigger').trigger('click')
|
||||
const item = wrapper.get('[role="menuitem"]')
|
||||
expect((item.element as HTMLButtonElement).disabled).toBe(false)
|
||||
await item.trigger('click')
|
||||
expect(execute).toHaveBeenCalledWith('editor.import-note-properties')
|
||||
expect(wrapper.find('[role="menu"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('写作模式显示入口但禁止绕过编辑器能力', async () => {
|
||||
const editor = useEditorStore()
|
||||
editor.mode = 'wysiwyg'
|
||||
editor.currentFilePath = '/示例.md'
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('.menu-trigger').trigger('click')
|
||||
expect((wrapper.get('[role="menuitem"]').element as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(wrapper.text()).toContain('请在无冲突的 Markdown 源码笔记中使用')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { executeEditorCommand } from '@/services/editorCommandService'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const editor = useEditorStore()
|
||||
const open = ref(false)
|
||||
const menu = ref<HTMLElement | null>(null)
|
||||
const trigger = ref<HTMLButtonElement | null>(null)
|
||||
const importItem = ref<HTMLButtonElement | null>(null)
|
||||
const error = ref('')
|
||||
const importEnabled = computed(() => editor.mode === 'source' && !!editor.currentFilePath
|
||||
&& !['conflict', 'external_changed'].includes(editor.saveStatus))
|
||||
|
||||
function dismiss(event: PointerEvent) {
|
||||
if (!menu.value?.contains(event.target as Node)) open.value = false
|
||||
}
|
||||
|
||||
async function showAndFocus() {
|
||||
open.value = true
|
||||
await nextTick()
|
||||
importItem.value?.focus()
|
||||
}
|
||||
|
||||
function closeAndFocus() {
|
||||
open.value = false
|
||||
trigger.value?.focus()
|
||||
}
|
||||
|
||||
async function importProperties() {
|
||||
if (!importEnabled.value) return
|
||||
const result = await executeEditorCommand('editor.import-note-properties')
|
||||
if (!result.ok) error.value = t('当前编辑器无法执行属性导入。', 'Property import is unavailable in the current editor.')
|
||||
else { error.value = ''; open.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('pointerdown', dismiss))
|
||||
onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="menu" class="titlebar-menu">
|
||||
<button ref="trigger" type="button" class="menu-trigger" aria-haspopup="menu" :aria-expanded="open" @click="open ? closeAndFocus() : showAndFocus()" @keydown.down.prevent="showAndFocus">
|
||||
{{ t('段落', 'Paragraph') }}
|
||||
</button>
|
||||
<div v-if="open" class="menu-popover" role="menu" @keydown.esc.stop.prevent="closeAndFocus">
|
||||
<button ref="importItem" type="button" role="menuitem" :disabled="!importEnabled" @click="importProperties">
|
||||
{{ t('导入为笔记属性…', 'Import as note properties…') }}
|
||||
</button>
|
||||
<small v-if="!importEnabled">{{ t('请在无冲突的 Markdown 源码笔记中使用', 'Available in a conflict-free Markdown source note') }}</small>
|
||||
<small v-if="error" role="alert">{{ error }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titlebar-menu { position: relative; flex: 0 0 auto; -webkit-app-region: no-drag; }
|
||||
.menu-trigger { min-height: 28px; padding: 3px 10px; border: 1px solid transparent; border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
||||
.menu-trigger:hover, .menu-trigger[aria-expanded='true'] { border-color: var(--color-border-default); background: var(--color-background-hover); color: var(--color-text-primary); }
|
||||
.menu-popover { position: absolute; top: calc(100% + 5px); left: 0; z-index: calc(var(--z-titlebar) + 2); display: grid; min-width: 220px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-elevated); box-shadow: var(--shadow-lg); }
|
||||
.menu-popover button { width: 100%; padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); text-align: left; white-space: nowrap; }
|
||||
.menu-popover button:hover:not(:disabled), .menu-popover button:focus-visible { background: var(--color-accent-soft); color: var(--color-accent-primary); }
|
||||
.menu-popover button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.menu-popover small { padding: var(--space-xs) var(--space-md); color: var(--color-text-tertiary); white-space: normal; }
|
||||
.menu-popover small[role='alert'] { color: var(--color-error); }
|
||||
</style>
|
||||
@@ -33,7 +33,7 @@ const workspaceStore = useWorkspaceStore()
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workspace-chat-launcher { position: absolute; right: 24px; bottom: 76px; z-index: 11; width: 42px; height: 42px; border-radius: var(--radius-full); background: var(--color-editor-scroll-background); color: var(--color-editor-scroll-text); box-shadow: var(--shadow-sm); }
|
||||
.workspace-chat-launcher { position: absolute; right: 72px; bottom: 24px; z-index: 11; width: 42px; height: 42px; border-radius: var(--radius-full); background: var(--color-editor-scroll-background); color: var(--color-editor-scroll-text); box-shadow: var(--shadow-sm); }
|
||||
|
||||
.workspace-view {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user