修复: 演示元素消失+画布错位+PDF横版导出+工具栏信息架构重构
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<!-- =====================================================================
|
||||
Toolbar.vue — 顶部工具栏
|
||||
信息架构分层:高频创作动作直接暴露;文件域(库/存取/导出)收进「文件」菜单
|
||||
===================================================================== -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { store } from '../../core/store'
|
||||
import { themes } from '../../core/sample'
|
||||
|
||||
@@ -10,7 +11,6 @@ const emit = defineEmits<{
|
||||
(e: 'present'): void
|
||||
(e: 'open-library'): void
|
||||
(e: 'open-settings'): void
|
||||
(e: 'open-ai'): void
|
||||
(e: 'save'): void
|
||||
(e: 'open-templates'): void
|
||||
(e: 'export-json'): void
|
||||
@@ -44,11 +44,49 @@ function action(a: string) {
|
||||
case 'present': emit('present'); break
|
||||
case 'library': emit('open-library'); break
|
||||
case 'save': emit('save'); break
|
||||
case 'open-ai': emit('open-ai'); break
|
||||
case 'settings': emit('open-settings'); break
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 文件菜单(低频动作收纳) ---------- */
|
||||
const fileMenuOpen = ref(false)
|
||||
const fileMenuBtn = ref<HTMLElement>()
|
||||
|
||||
function toggleFileMenu() {
|
||||
fileMenuOpen.value = !fileMenuOpen.value
|
||||
}
|
||||
|
||||
function fileAction(a: string) {
|
||||
fileMenuOpen.value = false
|
||||
switch (a) {
|
||||
case 'library': emit('open-library'); break
|
||||
case 'save': emit('save'); break
|
||||
case 'export-json': emit('export-json'); break
|
||||
case 'import-materials': emit('import-materials'); break
|
||||
case 'import-json': emit('import-json'); break
|
||||
case 'export-pdf': emit('export-pdf'); break
|
||||
case 'reset': action('reset'); break
|
||||
}
|
||||
}
|
||||
|
||||
function onDocClick(e: MouseEvent) {
|
||||
if (fileMenuOpen.value && fileMenuBtn.value && !fileMenuBtn.value.contains(e.target as Node)) {
|
||||
fileMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && fileMenuOpen.value) fileMenuOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousedown', onDocClick)
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousedown', onDocClick)
|
||||
document.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
|
||||
defineProps<{ disabledActions?: string[] }>()
|
||||
</script>
|
||||
|
||||
@@ -59,12 +97,17 @@ defineProps<{ disabledActions?: string[] }>()
|
||||
<span class="name">u-ppt</span>
|
||||
<span class="sub">在线演示工具</span>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<button class="btn" data-action="add-slide" title="新建幻灯片" :disabled="disabledActions?.includes('add-slide')" @click="action('add-slide')">+ 幻灯片</button>
|
||||
<!-- 高频:页面操作 -->
|
||||
<button class="btn tool-add" data-action="add-slide" title="新建幻灯片" :disabled="disabledActions?.includes('add-slide')" @click="action('add-slide')">+ 幻灯片</button>
|
||||
<button class="btn" data-action="templates" title="从模板新建页" @click="emit('open-templates')">📋 模板</button>
|
||||
<button class="btn" data-action="dup-slide" title="复制当前页" :disabled="disabledActions?.includes('dup-slide')" @click="action('dup-slide')">⎘ 复制</button>
|
||||
<button class="btn" data-action="del-slide" title="删除当前页" :disabled="disabledActions?.includes('del-slide')" @click="action('del-slide')">🗑 删除</button>
|
||||
<span class="sep"></span>
|
||||
<button class="btn icon-only" data-action="dup-slide" title="复制当前页" :disabled="disabledActions?.includes('dup-slide')" @click="action('dup-slide')">⎘</button>
|
||||
<button class="btn icon-only danger-hover" data-action="del-slide" title="删除当前页" :disabled="disabledActions?.includes('del-slide')" @click="action('del-slide')">🗑</button>
|
||||
<span class="sep"></span>
|
||||
|
||||
<!-- 主题 -->
|
||||
<label class="theme-select">
|
||||
主题
|
||||
<select v-model="currentTheme" @change="onThemeChange">
|
||||
@@ -72,16 +115,94 @@ defineProps<{ disabledActions?: string[] }>()
|
||||
</select>
|
||||
</label>
|
||||
<span class="sep"></span>
|
||||
<button class="btn" data-action="library" title="我的演示文库" @click="action('library')">📁 文库</button>
|
||||
<button class="btn" data-action="save" title="保存到文库" :disabled="disabledActions?.includes('save')" @click="action('save')">💾 保存</button>
|
||||
<button class="btn" data-action="export-json" title="导出 JSON" @click="emit('export-json')">📤 导出</button>
|
||||
<button class="btn" data-action="import-materials" title="导入本地资料(图片/文档/目录)" @click="emit('import-materials')">📂 资料</button>
|
||||
<button class="btn" data-action="import-json" title="导入 JSON" @click="emit('import-json')">📥 导入</button>
|
||||
<button class="btn" data-action="export-pdf" title="打印/导出 PDF" @click="emit('export-pdf')">🖨 PDF</button>
|
||||
<button class="btn" data-action="open-ai" title="打开 AI 助手" @click="action('open-ai')">🤖 AI</button>
|
||||
<button class="btn" data-action="reset" title="重置为内置示例" :disabled="disabledActions?.includes('reset')" @click="action('reset')">⟲ 重置</button>
|
||||
|
||||
<!-- 低频:文件域收进菜单 -->
|
||||
<div class="file-menu" ref="fileMenuBtn">
|
||||
<button class="btn" data-action="file-menu" :class="{ active: fileMenuOpen }" @click="toggleFileMenu">
|
||||
🗀 文件 <span class="chevron" :class="{ open: fileMenuOpen }">▾</span>
|
||||
</button>
|
||||
<Transition name="menu-pop">
|
||||
<div v-if="fileMenuOpen" class="file-dropdown" role="menu">
|
||||
<button class="menu-item" role="menuitem" @click="fileAction('library')">
|
||||
<span class="mi-icon">📁</span><span class="mi-label">文库</span><span class="mi-hint">我的演示</span>
|
||||
</button>
|
||||
<button class="menu-item" role="menuitem" :disabled="disabledActions?.includes('save')" @click="fileAction('save')">
|
||||
<span class="mi-icon">💾</span><span class="mi-label">保存到文库</span><span class="mi-hint">已自动暂存</span>
|
||||
</button>
|
||||
<div class="menu-sep"></div>
|
||||
<button class="menu-item" role="menuitem" @click="fileAction('import-materials')">
|
||||
<span class="mi-icon">📂</span><span class="mi-label">导入资料</span><span class="mi-hint">图片 / 文档 / 目录</span>
|
||||
</button>
|
||||
<button class="menu-item" role="menuitem" @click="fileAction('import-json')">
|
||||
<span class="mi-icon">📥</span><span class="mi-label">导入 JSON</span>
|
||||
</button>
|
||||
<button class="menu-item" role="menuitem" @click="fileAction('export-json')">
|
||||
<span class="mi-icon">📤</span><span class="mi-label">导出 JSON</span>
|
||||
</button>
|
||||
<button class="menu-item" role="menuitem" @click="fileAction('export-pdf')">
|
||||
<span class="mi-icon">🖨</span><span class="mi-label">导出 PDF</span><span class="mi-hint">打印 / 另存</span>
|
||||
</button>
|
||||
<div class="menu-sep"></div>
|
||||
<button class="menu-item danger" role="menuitem" :disabled="disabledActions?.includes('reset')" @click="fileAction('reset')">
|
||||
<span class="mi-icon">⟲</span><span class="mi-label">重置为示例</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- 终端动作 -->
|
||||
<button class="btn primary" data-action="present" title="开始演示 (F5)" :disabled="disabledActions?.includes('present')" @click="action('present')">▶ 演示</button>
|
||||
<button class="btn ghost" data-action="settings" title="AI 设置" @click="action('settings')">⚙</button>
|
||||
<button class="btn ghost icon-only" data-action="settings" title="AI 设置" @click="action('settings')">⚙</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 新建幻灯片:创作起点,给主色轻底强调 */
|
||||
.tool-add { background: var(--ui-primary-soft); border-color: transparent; color: var(--ui-primary); font-weight: 500; }
|
||||
.tool-add:hover { background: #e0e7ff; }
|
||||
.tool-add:disabled { opacity: .5; }
|
||||
|
||||
/* 纯图标按钮:删除/复制降为图标,减文字噪音 */
|
||||
.btn.icon-only { width: 36px; padding: 0; justify-content: center; font-size: 16px; }
|
||||
.btn.danger-hover:hover { color: var(--ui-danger); border-color: var(--ui-danger); background: rgba(225, 29, 72, .05); }
|
||||
|
||||
/* ---------- 文件菜单 ---------- */
|
||||
.file-menu { position: relative; display: inline-flex; }
|
||||
.file-menu .btn.active { background: var(--ui-hover); border-color: var(--ui-primary); }
|
||||
.chevron { font-size: 16px; margin-left: 2px; opacity: .6; transition: transform .15s; }
|
||||
.chevron.open { transform: rotate(180deg); }
|
||||
|
||||
.file-dropdown {
|
||||
position: absolute; top: calc(100% + 6px); left: 0;
|
||||
width: max-content; min-width: 216px;
|
||||
background: var(--ui-panel);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
}
|
||||
.menu-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
width: 100%; padding: 7px 10px;
|
||||
border: none; background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px; color: var(--ui-text);
|
||||
text-align: left; cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu-item:hover { background: var(--ui-hover); }
|
||||
.menu-item:disabled { opacity: .45; cursor: not-allowed; background: transparent; }
|
||||
.menu-item .mi-icon { width: 18px; text-align: center; flex-shrink: 0; }
|
||||
.menu-item .mi-label { flex-shrink: 0; }
|
||||
.menu-item .mi-hint { margin-left: auto; font-size: 11px; color: var(--ui-muted); padding-left: 14px; }
|
||||
.menu-item.danger { color: var(--ui-danger); }
|
||||
.menu-item.danger:hover { background: rgba(225, 29, 72, .06); }
|
||||
.menu-sep { height: 1px; background: var(--ui-border); margin: 5px 8px; }
|
||||
|
||||
/* 弹出动画 */
|
||||
.menu-pop-enter-active { transition: opacity .14s, transform .14s; }
|
||||
.menu-pop-leave-active { transition: opacity .1s, transform .1s; }
|
||||
.menu-pop-enter-from, .menu-pop-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user