feat: 顶部工具栏与底部缩放胶囊
- TopBar: 左工具组(选择/文字/矩形/图片,切换走命令表,键位 tooltip)、 中画布尺寸(预设/比例锁/自定义宽高,逻辑从 CanvasPanel 搬来)、 右撤销重做(禁用态随 canUndo/canRedo)+ 导出入口(滚到右栏导出区) - BottomDock: 悬浮缩放胶囊,±/百分比菜单(50/100/200、适应、缩放到选中) 与适应画布按钮;有命令的一律走命令表,任意数值档走 zoomAt 锚点缩放 - 图标全部内联 SVG,不引依赖
This commit is contained in:
+230
-11
@@ -1,19 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 【T7 待实现 — 底部悬浮胶囊】
|
||||
* 【T7 — 底部悬浮胶囊】
|
||||
*
|
||||
* 浮在画布下缘居中的圆角胶囊,装高频快捷操作:
|
||||
* 缩放百分比(可点开选 50/100/200%、适应画布、缩放到选中)、减号/加号、
|
||||
* 适应画布按钮。全部走 `runCommand("zoomFit")` 等命令,不要自己调 viewport。
|
||||
* 浮在画布区下缘居中的圆角胶囊,装高频快捷操作:
|
||||
* − | 缩放百分比(点开选 50% / 100% / 200% / 适应画布 / 缩放到选中)| +
|
||||
* ⤢ 适应画布
|
||||
*
|
||||
* 缩放数值读 `useViewport().zoom`。
|
||||
* 它是浮层,绝对定位在 .area-stage 里,不占布局空间——不要给它加 margin 去挤画布。
|
||||
* 缩放数值读 `useViewport().zoom`(revision 无关,zoom 是 vue ref,会跟着
|
||||
* 视口变化自动更新)。它是**浮层**,绝对定位在 .area-stage 里,不占布局空间。
|
||||
*
|
||||
* 命令路由:有对应命令的一律 `runCommand(...)`(zoomIn / zoomOut / zoomReset /
|
||||
* zoomFit / zoomSelection),和快捷键共用一张表。只有 50% / 200% 这两个任意数值
|
||||
* 档在命令表里没有对应条目(命令表只覆盖「相对缩放 + 复位 + 适应」这些离散动作),
|
||||
* 所以它们走 useViewport().zoomAt 以画布中心为锚做绝对缩放——不重复实现任何
|
||||
* 变换逻辑,只是把「目标百分比 ÷ 当前 zoom」这个相对因子喂给现成的锚点缩放。
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import { useViewport } from "@/editor/useViewport";
|
||||
import { runCommand } from "@/editor/commands";
|
||||
import { keyHintOf } from "@/editor/useShortcuts";
|
||||
|
||||
const { zoom, zoomAt, MIN_ZOOM, MAX_ZOOM } = useViewport();
|
||||
const { canvas } = useEditor();
|
||||
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const menuOpen = ref(false);
|
||||
|
||||
const pct = computed(() => Math.round(zoom.value * 100));
|
||||
const canZoomOut = computed(() => zoom.value > MIN_ZOOM * 1.001);
|
||||
const canZoomIn = computed(() => zoom.value < MAX_ZOOM * 0.999);
|
||||
|
||||
/** 以画布视口中心为锚,缩放到指定的绝对百分比。 */
|
||||
function zoomToPercent(target: number) {
|
||||
const c = canvas.value;
|
||||
if (!c) return;
|
||||
zoomAt(c.getWidth() / 2, c.getHeight() / 2, target / 100 / zoom.value);
|
||||
}
|
||||
|
||||
function close() {
|
||||
menuOpen.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点胶囊/菜单以外任意处(含画布)关闭。**不拦截事件本身**——画布交互照常发生,
|
||||
* 只是先把菜单收掉(和 Figma 一致)。不能靠全屏 backdrop:
|
||||
* .dock 带 transform,会把 position:fixed 的含块变成胶囊自身,盖不住全屏。
|
||||
*/
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (menuOpen.value && root.value && !root.value.contains(e.target as Node)) close();
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") close();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("pointerdown", onPointerDown);
|
||||
window.addEventListener("keydown", onKeydown);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("pointerdown", onPointerDown);
|
||||
window.removeEventListener("keydown", onKeydown);
|
||||
});
|
||||
|
||||
/* ================================================================ 图标 */
|
||||
const ICON_ATTRS = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
minus: `<svg ${ICON_ATTRS}><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
||||
plus: `<svg ${ICON_ATTRS}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
||||
chevron: `<svg ${ICON_ATTRS}><path d="m6 9 6 6 6-6"/></svg>`,
|
||||
fit: `<svg ${ICON_ATTRS}><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>`,
|
||||
};
|
||||
|
||||
/** 当前 zoom 是否正好是某个菜单数值档(±0.05% 容差),用于打勾。 */
|
||||
function atLevel(target: number): boolean {
|
||||
return Math.abs(zoom.value - target / 100) < 0.0005;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dock">
|
||||
<span class="todo">T7 待实现:缩放胶囊</span>
|
||||
<div class="dock" ref="root">
|
||||
<!-- 缩放 − / + -->
|
||||
<button class="zbtn" title="缩小" aria-label="缩小" :disabled="!canZoomOut" @click="runCommand('zoomOut')" v-html="icons.minus" />
|
||||
|
||||
<!-- 缩放百分比 → 小菜单 -->
|
||||
<button class="pct" :class="{ open: menuOpen }" title="缩放级别" aria-label="缩放级别" @click="menuOpen = !menuOpen">
|
||||
<span class="pct-num">{{ pct }}%</span>
|
||||
<span class="pct-caret" v-html="icons.chevron"></span>
|
||||
</button>
|
||||
|
||||
<div v-if="menuOpen" class="menu" role="menu">
|
||||
<button role="menuitem" class="mrow" @click="close(); zoomToPercent(50)">
|
||||
<span class="mark" aria-hidden="true">{{ atLevel(50) ? "✓" : "" }}</span>
|
||||
<span>50%</span>
|
||||
</button>
|
||||
<button role="menuitem" class="mrow" @click="close(); runCommand('zoomReset')">
|
||||
<span class="mark" aria-hidden="true">{{ atLevel(100) ? "✓" : "" }}</span>
|
||||
<span>100%</span>
|
||||
<span class="hint">{{ keyHintOf('zoomReset') }}</span>
|
||||
</button>
|
||||
<button role="menuitem" class="mrow" @click="close(); zoomToPercent(200)">
|
||||
<span class="mark" aria-hidden="true">{{ atLevel(200) ? "✓" : "" }}</span>
|
||||
<span>200%</span>
|
||||
</button>
|
||||
|
||||
<div class="msep" role="separator"></div>
|
||||
|
||||
<button role="menuitem" class="mrow" @click="close(); runCommand('zoomFit')">
|
||||
<span class="mark" aria-hidden="true"></span>
|
||||
<span>适应画布</span>
|
||||
<span class="hint">{{ keyHintOf('zoomFit') }}</span>
|
||||
</button>
|
||||
<button role="menuitem" class="mrow" @click="close(); runCommand('zoomSelection')">
|
||||
<span class="mark" aria-hidden="true"></span>
|
||||
<span>缩放到选中</span>
|
||||
<span class="hint">{{ keyHintOf('zoomSelection') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="zbtn" title="放大" aria-label="放大" :disabled="!canZoomIn" @click="runCommand('zoomIn')" v-html="icons.plus" />
|
||||
|
||||
<span class="vsep" aria-hidden="true"></span>
|
||||
|
||||
<!-- 适应画布 -->
|
||||
<button class="zbtn fit" title="适应画布(Shift+1)" aria-label="适应画布" @click="runCommand('zoomFit')" v-html="icons.fit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,16 +135,123 @@
|
||||
left: 50%;
|
||||
bottom: 16px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
gap: 2px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--bg3);
|
||||
box-shadow: 0 6px 24px rgb(0 0 0 / 0.45);
|
||||
}
|
||||
.todo {
|
||||
|
||||
/* ---- 缩放 ± 与适应画布按钮 ---- */
|
||||
.zbtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.zbtn:hover:not(:disabled) {
|
||||
background: var(--bg2);
|
||||
color: var(--fg);
|
||||
}
|
||||
.zbtn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vsep {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--line);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* ---- 百分比按钮 ---- */
|
||||
.pct {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 26px;
|
||||
min-width: 64px;
|
||||
padding: 0 6px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
}
|
||||
.pct:hover,
|
||||
.pct.open {
|
||||
background: var(--bg2);
|
||||
}
|
||||
.pct-num {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pct-caret {
|
||||
display: inline-flex;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
/* ---- 缩放菜单 ---- */
|
||||
.menu {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 10px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
min-width: 172px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--bg3);
|
||||
box-shadow: 0 8px 28px rgb(0 0 0 / 0.5);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.mrow:hover {
|
||||
background: var(--bg2);
|
||||
color: var(--fg);
|
||||
}
|
||||
.mrow .mark {
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
}
|
||||
.mrow .hint {
|
||||
margin-left: auto;
|
||||
color: var(--fg-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.msep {
|
||||
height: 1px;
|
||||
background: var(--line);
|
||||
margin: 4px 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+300
-11
@@ -1,32 +1,321 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 【T7 待实现 — 顶部工具栏】
|
||||
* 【T7 — 顶部工具栏】
|
||||
*
|
||||
* 左:工具组(选择 V / 文字 T / 矩形 R / 图片 Shift+K),当前工具高亮。
|
||||
* 工具状态是 `useEditor().tool`,切换走 `runCommand("toolSelect")` 等命令——
|
||||
* **不要直接写 tool.value**,否则工具栏和快捷键会走成两条路。
|
||||
* 三段式(grid 1fr auto 1fr,中间严格居中):
|
||||
*
|
||||
* 中:文件名 / 画布尺寸(尺寸预设与自定义从旧的 CanvasPanel 搬过来,那是 T10 的活)。
|
||||
* 右:撤销 / 重做(`useHistory` 的 canUndo / canRedo 控制禁用态)、导出入口。
|
||||
* 左 —— 工具组。选择 V / 文字 T / 矩形 R / 图片 Shift+K。
|
||||
* 当前工具高亮读 `useEditor().tool`;切换一律 `runCommand(...)`,不直接写
|
||||
* `tool.value`——工具栏与快捷键共用命令表,抄一份必然走偏。每个按钮的 tooltip
|
||||
* 用 `keyHintOf(commandId)` 显示键位,保证键位只在一处(commands.ts)维护。
|
||||
*
|
||||
* 中 —— 画布尺寸。逻辑搬自旧的 CanvasPanel.vue(T10 的活,只读不复写):
|
||||
* 预设下拉、比例锁下拉、自定义宽高。预设表直接用 @/editor/canvasSize 的
|
||||
* SIZE_PRESETS / RATIOS / applyRatio。
|
||||
*
|
||||
* 右 —— 撤销 / 重做(useHistory 的 canUndo / canRedo 控制禁用态)+ 导出入口。
|
||||
* 导出面板本体是 T10 的事,这里只把右栏下半的滚动容器(.right-bottom,T10 的
|
||||
* ToolPanels 就在里面)滚到底——导出与模板组在 ToolPanels 里是常驻组。
|
||||
*/
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import { useHistory } from "@/editor/useHistory";
|
||||
import { RATIOS, SIZE_PRESETS, applyRatio } from "@/editor/canvasSize";
|
||||
import { runCommand } from "@/editor/commands";
|
||||
import { keyHintOf } from "@/editor/useShortcuts";
|
||||
import type { ToolId } from "@/types";
|
||||
|
||||
const { tool, size, setSize } = useEditor();
|
||||
const { canUndo, canRedo } = useHistory();
|
||||
|
||||
/* ================================================================ 图标
|
||||
* 仓里不引图标库,用内联 SVG 手画(都是简单形状)。stroke 用 currentColor,
|
||||
* 颜色跟着按钮的 CSS 走。
|
||||
*/
|
||||
const ICON_ATTRS = 'width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
select: `<svg ${ICON_ATTRS}><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="M13 13l6 6"/></svg>`,
|
||||
text: `<svg ${ICON_ATTRS}><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>`,
|
||||
rect: `<svg ${ICON_ATTRS}><rect x="4.5" y="4.5" width="15" height="15" rx="1.5"/></svg>`,
|
||||
image: `<svg ${ICON_ATTRS}><rect x="3.5" y="3.5" width="17" height="17" rx="2"/><circle cx="9" cy="9" r="1.6"/><path d="M21 15l-4.6-4.6L6 21"/></svg>`,
|
||||
undo: `<svg ${ICON_ATTRS}><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5 0 0 1-5.5 5.5H11"/></svg>`,
|
||||
redo: `<svg ${ICON_ATTRS}><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></svg>`,
|
||||
export: `<svg ${ICON_ATTRS}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
|
||||
};
|
||||
|
||||
/* ================================================================ 工具组 */
|
||||
|
||||
/**
|
||||
* `cmd` 是命令表里的 id(字符串**原样**写在文件里,scripts 侧的 Node 断言会
|
||||
* 核对每个 id 都存在于 COMMANDS——拼错一个就是「按钮点了没反应」)。active 态
|
||||
* 由 `toolOf(cmd)` 映射回 useEditor().tool 的值来判定。
|
||||
*/
|
||||
const TOOLS = [
|
||||
{ cmd: "toolSelect", label: "选择工具", icon: icons.select },
|
||||
{ cmd: "toolText", label: "文字工具", icon: icons.text },
|
||||
{ cmd: "toolRect", label: "矩形工具", icon: icons.rect },
|
||||
{ cmd: "toolImage", label: "图片工具", icon: icons.image },
|
||||
] as const;
|
||||
|
||||
const TOOL_OF: Record<string, ToolId> = {
|
||||
toolSelect: "select",
|
||||
toolText: "text",
|
||||
toolRect: "rect",
|
||||
toolImage: "image",
|
||||
};
|
||||
|
||||
function toolOf(cmd: string): ToolId {
|
||||
return TOOL_OF[cmd] ?? "select";
|
||||
}
|
||||
|
||||
function tip(cmd: string, label: string): string {
|
||||
const k = keyHintOf(cmd);
|
||||
return k ? `${label}(${k})` : label;
|
||||
}
|
||||
|
||||
/* ================================================================ 画布尺寸(中) */
|
||||
|
||||
/** 选中预设后,任何一边宽高的改动都会把下拉弹回「自定义」。 */
|
||||
const presetId = ref<string>("custom");
|
||||
/** 比例锁。改宽高时另一侧按这个比例联动;"free"(0)表示不锁。 */
|
||||
const ratioId = ref<string>("free");
|
||||
|
||||
const ratio = computed(() => RATIOS.find((r) => r.id === ratioId.value)?.value ?? 0);
|
||||
|
||||
const currentPreset = computed(() => SIZE_PRESETS.find((p) => p.id === presetId.value));
|
||||
|
||||
/** 尺寸可能被别处改掉(模板加载、别的面板),下拉常驻,要跟着真值走。 */
|
||||
watch(
|
||||
() => size.value,
|
||||
(s) => {
|
||||
const p = SIZE_PRESETS.find((x) => x.width === s.width && x.height === s.height);
|
||||
presetId.value = p ? p.id : "custom";
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function pickPreset(id: string) {
|
||||
presetId.value = id;
|
||||
const p = SIZE_PRESETS.find((s) => s.id === id);
|
||||
if (p) setSize({ width: p.width, height: p.height });
|
||||
}
|
||||
|
||||
/** 手动改宽/高。带比例锁时另一侧按 applyRatio 联动(和旧 CanvasPanel 一致)。 */
|
||||
function edit(dimension: "width" | "height", raw: string) {
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n) || n < 16) return;
|
||||
presetId.value = "custom";
|
||||
setSize(applyRatio({ ...size.value, [dimension]: n }, ratio.value, dimension));
|
||||
}
|
||||
|
||||
/** 预设下拉里只放短名,完整「名字(宽×高)」放 title,随鼠标悬停可见。 */
|
||||
function shortPresetLabel(label: string): string {
|
||||
return label.split("(")[0].trim();
|
||||
}
|
||||
|
||||
const presetTitle = computed(() => {
|
||||
const p = currentPreset.value;
|
||||
return p ? `${p.label}(${p.width}×${p.height})` : "自定义尺寸";
|
||||
});
|
||||
|
||||
/* ================================================================ 导出入口(右) */
|
||||
|
||||
/** 右栏下半是滚动容器,导出与模板组常驻在最底。滚到底露出导出区。
|
||||
* T10 落地前那里是空的,滚动是幂等操作,先这么挂着。 */
|
||||
function scrollToExport() {
|
||||
const scroller = document.querySelector<HTMLElement>(".right-bottom");
|
||||
if (!scroller) return;
|
||||
scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<span class="todo">T7 待实现:工具组 / 画布尺寸 / 撤销重做 / 导出</span>
|
||||
<!-- 左:工具组 -->
|
||||
<div class="zone left">
|
||||
<button
|
||||
v-for="t in TOOLS"
|
||||
:key="t.cmd"
|
||||
class="tbtn"
|
||||
:class="{ active: toolOf(t.cmd) === tool }"
|
||||
:title="tip(t.cmd, t.label)"
|
||||
:aria-label="t.label"
|
||||
@click="runCommand(t.cmd)"
|
||||
v-html="t.icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 中:画布尺寸(预设 + 比例锁 + 自定义宽高) -->
|
||||
<div class="zone mid">
|
||||
<select
|
||||
class="preset"
|
||||
:value="presetId"
|
||||
:title="presetTitle"
|
||||
aria-label="画布尺寸预设"
|
||||
@change="pickPreset(($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option v-for="p in SIZE_PRESETS" :key="p.id" :value="p.id" :title="`${p.width}×${p.height}px`">
|
||||
{{ shortPresetLabel(p.label) }}
|
||||
</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
|
||||
<div class="dims">
|
||||
<input
|
||||
type="number"
|
||||
min="16"
|
||||
class="num"
|
||||
:value="size.width"
|
||||
title="宽度(px)"
|
||||
aria-label="画布宽度"
|
||||
@change="edit('width', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="times" aria-hidden="true">×</span>
|
||||
<input
|
||||
type="number"
|
||||
min="16"
|
||||
class="num"
|
||||
:value="size.height"
|
||||
title="高度(px)"
|
||||
aria-label="画布高度"
|
||||
@change="edit('height', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-model="ratioId"
|
||||
class="ratio"
|
||||
title="比例锁:编辑宽高时另一侧按此比例联动"
|
||||
aria-label="锁定比例"
|
||||
>
|
||||
<option v-for="r in RATIOS" :key="r.id" :value="r.id" :title="r.label">
|
||||
{{ shortPresetLabel(r.label) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 右:撤销 / 重做 / 导出 -->
|
||||
<div class="zone right">
|
||||
<button
|
||||
class="tbtn"
|
||||
:disabled="!canUndo"
|
||||
:title="tip('undo', '撤销')"
|
||||
aria-label="撤销"
|
||||
@click="runCommand('undo')"
|
||||
v-html="icons.undo"
|
||||
/>
|
||||
<button
|
||||
class="tbtn"
|
||||
:disabled="!canRedo"
|
||||
:title="tip('redo', '重做')"
|
||||
aria-label="重做"
|
||||
@click="runCommand('redo')"
|
||||
v-html="icons.redo"
|
||||
/>
|
||||
<span class="vr" aria-hidden="true"></span>
|
||||
<button
|
||||
class="tbtn"
|
||||
title="导出(面板在右侧,点了滚过去)"
|
||||
aria-label="导出"
|
||||
@click="scrollToExport"
|
||||
v-html="icons.export"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.topbar {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
height: var(--topbar-h);
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--bg2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.todo {
|
||||
|
||||
.zone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.mid {
|
||||
justify-content: center;
|
||||
}
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ---- 图标按钮(工具 / 撤销重做 / 导出共用) ---- */
|
||||
.tbtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.tbtn:hover {
|
||||
background: var(--bg3);
|
||||
color: var(--fg);
|
||||
}
|
||||
.tbtn:active {
|
||||
background: var(--bg);
|
||||
}
|
||||
.tbtn.active {
|
||||
background: var(--bg3);
|
||||
color: var(--accent);
|
||||
}
|
||||
.tbtn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
background: transparent;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
.vr {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--line);
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
/* ---- 画布尺寸 ---- */
|
||||
.preset {
|
||||
width: 112px;
|
||||
}
|
||||
.ratio {
|
||||
width: 84px;
|
||||
}
|
||||
|
||||
.dims {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 覆盖全局 input/select 的 width:100%,顶栏里宽度要收敛。 */
|
||||
.num {
|
||||
width: 58px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.times {
|
||||
color: var(--fg-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user