Files
teaching-design/docs/superpowers/specs/2026-06-22-merge-print-export-buttons-design.md

114 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 合并「打印整册」与「导出 MD」按钮设计
## 背景
`WorkspaceToolbar.vue` 当前并排放置两个独立按钮,均在 `lessonCount === 0` 时禁用:
```vue
<button data-testid="print" :disabled="lessonCount === 0" @click="$emit('print')">打印整册</button>
<button data-testid="export" :disabled="lessonCount === 0" @click="$emit('export')">导出 MD</button>
```
此前已经把「批量生成」「生成一篇」合并为一个下拉菜单按钮(`GenerateMenuButton.vue`2026-06-22 提交)。本次需求是用同样的交互模式合并「打印整册」「导出 MD」并借此机会把两个下拉菜单共用的逻辑抽取成通用组件避免重复。
## 交互
- 合并后的主按钮文案为「导出 ▾」。
- 点击主按钮只展开/收起下拉菜单,不直接触发任何操作。
- 下拉菜单包含「打印整册」「导出 MD」两项点击任意一项后触发对应事件并收起菜单。
- 点击外部区域或按 `Escape` 收起菜单(与生成菜单一致)。
- 主按钮在 `lessonCount === 0` 时整体禁用(原生 `disabled`),此时无法展开菜单——与现有两个按钮各自禁用的行为等价。
## 组件设计
### 新增通用组件 `src/components/ToolbarMenuButton.vue`
把下拉菜单的通用逻辑展开状态、点击外部关闭、Escape 关闭、disabled 处理)收进这一个组件,具体菜单项通过默认 slot 传入slot 透出 `close` 方法供菜单项点击后调用:
```ts
defineProps<{
label: string
toggleTestid: string
disabled?: boolean
}>()
```
行为:
- 内部 `open = ref(false)``toggle()` 切换 `open`(已经走过原生 `disabled` 拦截,无需在 JS 里再判断一次)。
- 根元素 `ref``onMounted` 注册 `document``click` 监听判断点击是否在组件外部,`onUnmounted` 移除;`keydown` 监听 `Escape` 关闭。
- 主按钮:`<button :data-testid="toggleTestid" :disabled="disabled" :aria-expanded="open" @click.stop="toggle">{{ label }}</button>`
- 菜单:`<ul v-if="open" class="toolbar-menu-list" role="menu"><slot :close="close" /></ul>`——因为主按钮 disabled 时浏览器不会触发其 click`open` 永远不会在 disabled 状态下变为 true不需要在 `v-if` 里重复判断 `!disabled`
- 根元素 class 由 `generate-menu` 改名为通用的 `toolbar-menu`
### `GenerateMenuButton.vue` 改为薄封装
```vue
<ToolbarMenuButton label="生成教案 ▾" toggle-testid="generate-menu-toggle">
<template #default="{ close }">
<li role="menuitem">
<button data-testid="batch-generate" @click="emit('batchGenerate'); close()">批量生成</button>
</li>
<li role="menuitem">
<button data-testid="generate" @click="emit('generate'); close()">生成一篇</button>
</li>
</template>
</ToolbarMenuButton>
```
对外接口(`data-testid` 值、按钮文案、`generate` / `batchGenerate` 事件)完全不变,`WorkspaceToolbar.vue``WorkspaceView.vue` 不需要任何改动。
### 新增 `src/components/ExportMenuButton.vue`
```vue
<script setup lang="ts">
defineProps<{ disabled?: boolean }>()
const emit = defineEmits<{ print: []; export: [] }>()
</script>
<template>
<ToolbarMenuButton label="导出 ▾" toggle-testid="export-menu-toggle" :disabled="disabled">
<template #default="{ close }">
<li role="menuitem">
<button data-testid="print" @click="emit('print'); close()">打印整册</button>
</li>
<li role="menuitem">
<button data-testid="export" @click="emit('export'); close()">导出 MD</button>
</li>
</template>
</ToolbarMenuButton>
</template>
```
### `WorkspaceToolbar.vue` 改动
将原来的 `<button data-testid="print">` / `<button data-testid="export">` 替换为:
```vue
<ExportMenuButton :disabled="lessonCount === 0" @print="$emit('print')" @export="$emit('export')" />
```
`defineEmits` 块(`print``export` 等)和 `WorkspaceView.vue` 的监听逻辑不变。
### 样式 (`src/style.css`)
- 把现有 `.generate-menu` / `.generate-menu-list` / `.generate-menu-list button` 三条规则改名为通用的 `.toolbar-menu` / `.toolbar-menu-list` / `.toolbar-menu-list button`,两个下拉按钮共用,不重复定义。
- 移动端媒体查询里的 `.workspace-toolbar .generate-menu { flex: 0 0 auto; }` 同样改名为 `.workspace-toolbar .toolbar-menu { flex: 0 0 auto; }`
- 不引入任何新的颜色/圆角取值,继续复用 `var(--line)``var(--radius-md)``var(--green-100)``var(--green-700)` 等既有变量。
## 测试改动
- 新增 `ToolbarMenuButton.test.ts`:覆盖默认收起、点击展开、点击菜单项后通过 slot 的 `close()` 收起、点击外部收起、`Escape` 收起、`disabled` 时主按钮不可点击(因此菜单永远不会展开)。
- `GenerateMenuButton.test.ts`:现有用例保持不变,唯一改动是把断言根元素 class 的那一行从 `div.generate-menu` 改成 `div.toolbar-menu`(实现细节变化,行为不变)。
- 新增 `ExportMenuButton.test.ts`:覆盖点击「打印整册」触发 `print` 并收起菜单、点击「导出 MD」触发 `export` 并收起菜单、`disabled``true` 时主按钮 `disabled` 属性存在。
- `WorkspaceToolbar.test.ts`
- `disables print, export and clear when there are no lessons` 用例改写——`lessonCount === 0` 时菜单项不在 DOM 里(菜单不会展开),改为断言 `export-menu-toggle``disabled` 属性存在,以及 `clear``disabled` 属性存在。
- 如需要保留对菜单项可点击性的验证,新增一条用例:在 `lessonCount` 大于 0 时展开菜单点击 `print` / `export`,确认事件被触发。
- `WorkspaceView.test.ts`:第 207 行 `await wrapper.get('[data-testid="export"]').trigger('click')` 之前补一步 `await wrapper.get('[data-testid="export-menu-toggle"]').trigger('click')`
## 范围说明
本次改动仅涉及 UI 层的按钮合并、通用下拉逻辑抽取,不涉及:
- 打印逻辑(`$emit('print')` 之后 `WorkspaceView.vue` 内部如何打印)或导出逻辑(`zipExporter` 相关代码)。
- 移动端窄屏样式的额外适配(沿用现有响应式规则,新按钮作为 flex item 自然换行)。
-`GenerateMenuButton.vue` 公开接口或文案的任何改动。