fix(desktop): 扩充菜单并应用功能页主题
This commit is contained in:
@@ -26,7 +26,7 @@ describe('桌面顶部段落菜单', () => {
|
||||
editor.currentFilePath = '/示例.md'
|
||||
editor.saveStatus = 'saved'
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
expect(wrapper.text()).toContain('文件编辑段落视图')
|
||||
expect(wrapper.text()).toContain('文件编辑段落格式视图主题帮助')
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
const item = wrapper.get('.import-properties')
|
||||
expect((item.element as HTMLButtonElement).disabled).toBe(false)
|
||||
@@ -51,7 +51,7 @@ describe('桌面顶部段落菜单', () => {
|
||||
it('标题命令传入对应级别并显示快捷键', async () => {
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
const headings = wrapper.findAll('[data-menu="paragraph"] [role="menuitem"]').filter(item => item.text().startsWith('标题'))
|
||||
const headings = wrapper.findAll('[data-menu="paragraph"] [role="menuitem"]').filter(item => item.text().includes('级标题'))
|
||||
await headings[1].trigger('click')
|
||||
expect(execute).toHaveBeenCalledWith('editor.heading', 2)
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { executeEditorCommand, getEditorCommandCapabilities, type EditorCommandId } from '@/services/editorCommandService'
|
||||
import { t } from '@/i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
type MenuName = 'file' | 'edit' | 'paragraph' | 'view'
|
||||
type MenuName = 'file' | 'edit' | 'paragraph' | 'format' | 'view' | 'theme' | 'help'
|
||||
const editor = useEditorStore()
|
||||
const theme = useThemeStore()
|
||||
const router = useRouter()
|
||||
const open = ref<MenuName | null>(null)
|
||||
const bar = ref<HTMLElement | null>(null)
|
||||
const error = ref('')
|
||||
@@ -32,6 +34,8 @@ async function command(id: EditorCommandId, params?: unknown) {
|
||||
async function save() { await editor.save(); close() }
|
||||
function setMode(mode: 'source' | 'wysiwyg') { editor.setMode(mode); close() }
|
||||
function toggleTheme() { theme.toggleTheme(); close() }
|
||||
function applyTheme(id: string) { theme.applyTheme(id); close() }
|
||||
function navigate(path: string) { void router.push(path); close() }
|
||||
|
||||
onMounted(() => window.addEventListener('pointerdown', dismiss))
|
||||
onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
@@ -42,6 +46,8 @@ onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
<div class="menu-group" data-menu="file">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'file'" aria-haspopup="menu" @click="toggle('file')" @keydown.down.prevent="focusFirst('file')">{{ t('文件', 'File') }}</button>
|
||||
<div v-if="open === 'file'" class="menu-popover" role="menu">
|
||||
<button role="menuitem" @click="navigate('/')"><span>{{ t('选择知识库…', 'Choose knowledge base…') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" :disabled="!editor.currentFilePath || ['saving','conflict','external_changed'].includes(editor.saveStatus)" @click="save">
|
||||
<span>{{ t('保存', 'Save') }}</span><kbd>Ctrl+S</kbd>
|
||||
</button>
|
||||
@@ -58,7 +64,7 @@ onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
<button class="menu-trigger" :aria-expanded="open === 'paragraph'" aria-haspopup="menu" @click="toggle('paragraph')" @keydown.down.prevent="focusFirst('paragraph')">{{ t('段落', 'Paragraph') }}</button>
|
||||
<div v-if="open === 'paragraph'" class="menu-popover paragraph-menu" role="menu">
|
||||
<button role="menuitem" :disabled="!enabled('editor.paragraph')" @click="command('editor.paragraph')"><span>{{ t('正文', 'Body text') }}</span></button>
|
||||
<button v-for="level in 3" :key="level" role="menuitem" :disabled="!enabled('editor.heading')" @click="command('editor.heading', level)"><span>{{ t(`标题 ${level}`, `Heading ${level}`) }}</span></button>
|
||||
<button v-for="level in 6" :key="level" role="menuitem" :disabled="!enabled('editor.heading')" @click="command('editor.heading', level)"><span>{{ t(`${level} 级标题`, `Heading ${level}`) }}</span><kbd>Ctrl+{{ level }}</kbd></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" :disabled="!enabled('editor.bullet-list')" @click="command('editor.bullet-list')"><span>{{ t('无序列表', 'Bullet list') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.ordered-list')" @click="command('editor.ordered-list')"><span>{{ t('有序列表', 'Ordered list') }}</span></button>
|
||||
@@ -72,13 +78,44 @@ onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
<small v-if="!enabled('editor.import-note-properties')">{{ t('请在无冲突的 Markdown 源码笔记中使用', 'Available in a conflict-free Markdown source note') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="format">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'format'" aria-haspopup="menu" @click="toggle('format')" @keydown.down.prevent="focusFirst('format')">{{ t('格式', 'Format') }}</button>
|
||||
<div v-if="open === 'format'" class="menu-popover" role="menu">
|
||||
<button role="menuitem" :disabled="!enabled('editor.bold')" @click="command('editor.bold')"><span>{{ t('加粗', 'Bold') }}</span><kbd>Ctrl+B</kbd></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.italic')" @click="command('editor.italic')"><span>{{ t('斜体', 'Italic') }}</span><kbd>Ctrl+I</kbd></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.strikethrough')" @click="command('editor.strikethrough')"><span>{{ t('删除线', 'Strikethrough') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.inline-code')" @click="command('editor.inline-code')"><span>{{ t('行内代码', 'Inline code') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" :disabled="!enabled('editor.code-block')" @click="command('editor.code-block')"><span>{{ t('代码块', 'Code block') }}</span><kbd>Ctrl+Shift+K</kbd></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.math-block')" @click="command('editor.math-block')"><span>{{ t('公式块', 'Math block') }}</span><kbd>Ctrl+Shift+M</kbd></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.callout')" @click="command('editor.callout', { type: 'NOTE', body: t('提示内容', 'Callout content') })"><span>{{ t('警告框', 'Callout') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.horizontal-rule')" @click="command('editor.horizontal-rule')"><span>{{ t('水平分割线', 'Horizontal rule') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="view">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'view'" aria-haspopup="menu" @click="toggle('view')" @keydown.down.prevent="focusFirst('view')">{{ t('视图', 'View') }}</button>
|
||||
<div v-if="open === 'view'" class="menu-popover" role="menu">
|
||||
<button role="menuitemradio" :aria-checked="editor.mode === 'wysiwyg'" @click="setMode('wysiwyg')"><span>✓ {{ t('写作模式', 'Writing mode') }}</span></button>
|
||||
<button role="menuitemradio" :aria-checked="editor.mode === 'source'" @click="setMode('source')"><span>✓ {{ t('源码模式', 'Source mode') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" @click="toggleTheme"><span>{{ theme.isDark ? t('浅色主题', 'Light theme') : t('深色主题', 'Dark theme') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.heading.fold-all')" @click="command('editor.heading.fold-all')"><span>{{ t('全部折叠标题', 'Fold all headings') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.heading.unfold-all')" @click="command('editor.heading.unfold-all')"><span>{{ t('全部展开标题', 'Unfold all headings') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="theme">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'theme'" aria-haspopup="menu" @click="toggle('theme')" @keydown.down.prevent="focusFirst('theme')">{{ t('主题', 'Theme') }}</button>
|
||||
<div v-if="open === 'theme'" class="menu-popover" role="menu">
|
||||
<button v-for="item in theme.allThemes" :key="item.theme_id" role="menuitemradio" :aria-checked="theme.currentThemeId === item.theme_id" @click="applyTheme(item.theme_id)"><span>{{ theme.currentThemeId === item.theme_id ? '✓ ' : '' }}{{ item.name }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" @click="toggleTheme"><span>{{ theme.isDark ? t('切换为浅色', 'Switch to light') : t('切换为深色', 'Switch to dark') }}</span></button>
|
||||
<button role="menuitem" @click="navigate('/themes')"><span>{{ t('管理主题…', 'Manage themes…') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="help">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'help'" aria-haspopup="menu" @click="toggle('help')" @keydown.down.prevent="focusFirst('help')">{{ t('帮助', 'Help') }}</button>
|
||||
<div v-if="open === 'help'" class="menu-popover" role="menu">
|
||||
<button role="menuitem" @click="navigate('/logs')"><span>{{ t('运行日志', 'Operation logs') }}</span></button>
|
||||
<button role="menuitem" @click="navigate('/settings')"><span>{{ t('设置与诊断', 'Settings and diagnostics') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="error" class="menu-error" role="alert">{{ error }}</span>
|
||||
|
||||
@@ -75,21 +75,23 @@ function toggleSource() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="community-page">
|
||||
<h1>社区目录</h1>
|
||||
<p>连接您选择的来源。安装后仍需独立启用和授权;关闭社区不影响本地编辑。</p>
|
||||
<section class="community-controls" aria-label="社区来源">
|
||||
<main class="community-page feature-page">
|
||||
<header class="feature-header">
|
||||
<div><h1>社区目录</h1><p>连接您选择的来源。安装后仍需独立启用和授权;关闭社区不影响本地编辑。</p></div>
|
||||
</header>
|
||||
<section class="community-panel panel" aria-label="社区来源与搜索">
|
||||
<div class="community-controls">
|
||||
<label>来源地址 <input v-model="url" placeholder="https://community.example.org" :disabled="busy" /></label>
|
||||
<button class="btn" :disabled="busy || !url.trim()" @click="inspectSource">检查来源与公钥</button>
|
||||
<label>已添加来源 <select v-model="selectedSource" @change="search"><option value="">请选择</option><option v-for="item in sources" :key="item.id" :value="item.id">{{ item.url }}{{ item.enabled ? '' : '(已停用)' }}</option></select></label>
|
||||
<button class="btn" :disabled="!selectedSource || busy" @click="toggleSource">启用 / 停用来源</button>
|
||||
</section>
|
||||
<section class="community-controls" aria-label="搜索目录">
|
||||
</div>
|
||||
<div class="community-controls">
|
||||
<label>关键词 <input v-model="query" @keydown.enter="search" /></label>
|
||||
<label>类别 <select v-model="kind"><option v-for="item in kinds" :key="item.id" :value="item.id">{{ item.label }}</option></select></label>
|
||||
<button class="btn btn-primary" :disabled="busy || !selectedSource" @click="search">搜索 / 刷新</button>
|
||||
<button v-if="busy" class="btn" @click="cancel">取消</button>
|
||||
</section>
|
||||
</div>
|
||||
<p v-if="busy" role="status">正在处理…</p>
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<p v-if="notice" role="status">{{ notice }}</p>
|
||||
@@ -102,6 +104,7 @@ function toggleSource() {
|
||||
<span v-if="item.withdrawn">已撤回</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="candidates.length">
|
||||
<h2>已保存的声明式候选</h2><p>这些候选尚未应用到人设、MCP 或模型运行配置。</p>
|
||||
<details v-for="item in candidates" :key="item.key"><summary>{{ item.key.replace('community-candidate:', '') }}</summary><pre>{{ item.value }}</pre><button class="btn" @click="removeCandidate(item.key)">删除候选</button></details>
|
||||
@@ -125,6 +128,10 @@ function toggleSource() {
|
||||
|
||||
<style scoped>
|
||||
.community-page { padding: var(--space-xl); overflow: auto; min-width: 0; }
|
||||
.feature-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-xl); }
|
||||
.feature-header h1, .feature-header p { margin: 0; }
|
||||
.feature-header p { margin-top: var(--space-xs); color: var(--color-text-secondary); }
|
||||
.community-panel { padding: var(--space-xl); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-surface-primary); }
|
||||
.community-controls { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-md); margin-block: var(--space-lg); }
|
||||
label { display: grid; gap: var(--space-xs); }
|
||||
input, select { color: var(--color-text-primary); background: var(--color-background-secondary); border: 1px solid var(--color-border-subtle); padding: var(--space-sm); }
|
||||
|
||||
Reference in New Issue
Block a user