t11-topbar: 工具组居中,画布尺寸收进下拉
- TopBar 重排为「左留空 / 中工具组 / 右历史+导出」 - 新增画布编辑下拉(CanvasMenu):预设/分辨率/比例,逻辑照搬旧中间组 - 形状/背景按钮改成本体+角标组合,本体切工具、角标开下拉 - 形状按钮图标跟随当前形状(shapeMeta) - 文字工具光标:往 body 写 dataset.tool,样式规则留给 style.css
This commit is contained in:
+173
-166
@@ -1,66 +1,64 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 【T7 — 顶部工具栏】
|
* 【T11 — 顶部工具栏】工具组居中,尺寸设置收进「画布编辑」下拉。
|
||||||
*
|
*
|
||||||
* 三段式(grid 1fr auto 1fr,中间严格居中):
|
* 三段式(grid 1fr auto 1fr,中间严格居中):
|
||||||
*
|
*
|
||||||
* 左 —— 工具组。选择 V / 文字 T / 矩形 R / 图片 Shift+K。
|
* 左 —— 留空(以后放文件名之类)。
|
||||||
|
* 中 —— 工具组五个按钮,这是顶栏的视觉主体:
|
||||||
|
* - 画布编辑:纯下拉(CanvasMenu),点开改预设 / 宽高 / 比例。
|
||||||
|
* - 选择 / 文字:普通按钮,切工具。
|
||||||
|
* - 形状 / 背景:本体切工具 + 右侧小角标开下拉(Figma 的形状工具样式)。
|
||||||
* 当前工具高亮读 `useEditor().tool`;切换一律 `runCommand(...)`,不直接写
|
* 当前工具高亮读 `useEditor().tool`;切换一律 `runCommand(...)`,不直接写
|
||||||
* `tool.value`——工具栏与快捷键共用命令表,抄一份必然走偏。每个按钮的 tooltip
|
* `tool.value`——工具栏与快捷键共用命令表,抄一份必然走偏。tooltip 用
|
||||||
* 用 `keyHintOf(commandId)` 显示键位,保证键位只在一处(commands.ts)维护。
|
* `keyHintOf(commandId)` 拼键位,保证键位只在 commands.ts 里维护。
|
||||||
* 图标从 `@/ui/icons` 取(hugeicons,构建期内联,见那个文件的说明)。
|
* 右 —— 撤销 / 重做 + 导出入口(导出按钮滚到右栏的导出面板)。
|
||||||
*
|
*
|
||||||
* 中 —— 画布尺寸:预设下拉、比例锁下拉、自定义宽高。预设表直接用
|
* 文字工具的光标:这里只往 `document.body.dataset.tool` 写当前工具,
|
||||||
* @/editor/canvasSize 的 SIZE_PRESETS / RATIOS / applyRatio。
|
* 真正的 `body[data-tool="text"] .stage { cursor: text; }` 规则在 style.css
|
||||||
*
|
* 里(body 不在本组件的 scoped 作用域内,够不到)。卸载时清掉。
|
||||||
* 右 —— 撤销 / 重做(useHistory 的 canUndo / canRedo 控制禁用态)+ 导出入口。
|
|
||||||
* 导出按钮只负责把右栏下半的滚动容器(.right-bottom,即 ToolPanels)滚到底,
|
|
||||||
* 面板本体在那里。
|
|
||||||
*/
|
*/
|
||||||
import { computed, nextTick, ref, watch } from "vue";
|
import { computed, nextTick, onBeforeUnmount, watch } from "vue";
|
||||||
import { useEditor } from "@/editor/useEditor";
|
import { useEditor } from "@/editor/useEditor";
|
||||||
import { useHistory } from "@/editor/useHistory";
|
import { useHistory } from "@/editor/useHistory";
|
||||||
import { RATIOS, SIZE_PRESETS, applyRatio } from "@/editor/canvasSize";
|
|
||||||
import { runCommand } from "@/editor/commands";
|
import { runCommand } from "@/editor/commands";
|
||||||
import { keyHintOf } from "@/editor/useShortcuts";
|
import { keyHintOf } from "@/editor/useShortcuts";
|
||||||
import type { ToolId } from "@/types";
|
import { shapeMeta } from "@/editor/shapes";
|
||||||
import {
|
import {
|
||||||
IconSelect,
|
IconBackgroundTool,
|
||||||
IconText,
|
IconCanvasEdit,
|
||||||
IconRect,
|
IconChevronDown,
|
||||||
IconImage,
|
|
||||||
IconUndo,
|
|
||||||
IconRedo,
|
|
||||||
IconExport,
|
IconExport,
|
||||||
|
IconRedo,
|
||||||
|
IconSelect,
|
||||||
|
IconTextTool,
|
||||||
|
IconUndo,
|
||||||
} from "@/ui/icons";
|
} from "@/ui/icons";
|
||||||
import { openToolGroup } from "@/ui/toolGroups";
|
import { openToolGroup } from "@/ui/toolGroups";
|
||||||
|
import Dropdown from "@/components/ui/Dropdown.vue";
|
||||||
|
import CanvasMenu from "@/components/topbar/CanvasMenu.vue";
|
||||||
|
import ShapeMenu from "@/components/topbar/ShapeMenu.vue";
|
||||||
|
import BackgroundMenu from "@/components/topbar/BackgroundMenu.vue";
|
||||||
|
|
||||||
const { tool, size, setSize } = useEditor();
|
const { tool, shapeKind } = useEditor();
|
||||||
const { canUndo, canRedo } = useHistory();
|
const { canUndo, canRedo } = useHistory();
|
||||||
|
|
||||||
/* ================================================================ 工具组 */
|
/** 形状按钮的图标跟着当前形状走(和 Figma 一致)。 */
|
||||||
|
const shapeIcon = computed(() => shapeMeta(shapeKind.value).icon);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `cmd` 是命令表里的 id(字符串**原样**写在文件里,scripts 侧的 Node 断言会
|
* 形状 / 背景是「本体 + 角标」的组合:点本体切工具,点角标开下拉。
|
||||||
* 核对每个 id 都存在于 COMMANDS——拼错一个就是「按钮点了没反应」)。active 态
|
* 本体点击时若下拉正开着,顺手关掉——本体在 Dropdown 的 root 里,外壳的
|
||||||
* 由 `toolOf(cmd)` 映射回 useEditor().tool 的值来判定。
|
* 「点外面关」管不到它,这里用 slot 给的 `toggle` 在 open 时再翻一次。
|
||||||
*/
|
*/
|
||||||
const TOOLS = [
|
function pickShapeTool(open: boolean, toggle: () => void) {
|
||||||
{ cmd: "toolSelect", label: "选择工具", icon: IconSelect },
|
runCommand("toolShape");
|
||||||
{ cmd: "toolText", label: "文字工具", icon: IconText },
|
if (open) toggle();
|
||||||
{ cmd: "toolShape", label: "形状工具", icon: IconRect },
|
}
|
||||||
{ cmd: "toolBackground", label: "背景工具", icon: IconImage },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const TOOL_OF: Record<string, ToolId> = {
|
function pickBackgroundTool(open: boolean, toggle: () => void) {
|
||||||
toolSelect: "select",
|
runCommand("toolBackground");
|
||||||
toolText: "text",
|
if (open) toggle();
|
||||||
toolShape: "shape",
|
|
||||||
toolBackground: "background",
|
|
||||||
};
|
|
||||||
|
|
||||||
function toolOf(cmd: string): ToolId {
|
|
||||||
return TOOL_OF[cmd] ?? "select";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tip(cmd: string, label: string): string {
|
function tip(cmd: string, label: string): string {
|
||||||
@@ -68,57 +66,28 @@ function tip(cmd: string, label: string): string {
|
|||||||
return k ? `${label}(${k})` : label;
|
return k ? `${label}(${k})` : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================ 画布尺寸(中) */
|
/* ---------------------------------------------------------------- 光标 */
|
||||||
|
|
||||||
/** 选中预设后,任何一边宽高的改动都会把下拉弹回「自定义」。 */
|
/**
|
||||||
const presetId = ref<string>("custom");
|
* 文字 / 形状工具的鼠标指针。规则在 style.css(body 够不到 scoped),
|
||||||
/** 比例锁。改宽高时另一侧按这个比例联动;"free"(0)表示不锁。 */
|
* 这里只负责同步 data 属性。`immediate` 让初始工具也写一次。
|
||||||
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(
|
watch(
|
||||||
() => size.value,
|
tool,
|
||||||
(s) => {
|
(t) => {
|
||||||
const p = SIZE_PRESETS.find((x) => x.width === s.width && x.height === s.height);
|
document.body.dataset.tool = t;
|
||||||
presetId.value = p ? p.id : "custom";
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
onBeforeUnmount(() => {
|
||||||
function pickPreset(id: string) {
|
delete document.body.dataset.tool;
|
||||||
presetId.value = id;
|
|
||||||
const p = SIZE_PRESETS.find((s) => s.id === id);
|
|
||||||
if (p) setSize({ width: p.width, height: p.height });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 手动改宽/高。带比例锁时另一侧按 applyRatio 联动。 */
|
|
||||||
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})` : "自定义尺寸";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ================================================================ 导出入口(右) */
|
/* ---------------------------------------------------------------- 导出 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 右栏下半是滚动容器,「导出与模板」是最后一组。
|
* 右栏下半是滚动容器,「导出与模板」是最后一组。先展开再滚,否则滚到底
|
||||||
* 先展开那一组再滚——四个组默认全收,只滚不展开的话滚到底看到的还是一行标题。
|
* 看到的还是一行标题。`nextTick` 等展开内容渲染进 DOM,`scrollHeight` 才是最终高度。
|
||||||
* `nextTick` 是等展开后的内容渲染进 DOM,`scrollHeight` 才是最终高度。
|
|
||||||
*/
|
*/
|
||||||
async function scrollToExport() {
|
async function scrollToExport() {
|
||||||
openToolGroup("export");
|
openToolGroup("export");
|
||||||
@@ -131,68 +100,107 @@ async function scrollToExport() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<!-- 左:工具组 -->
|
<!-- 左:留空(以后放文件名之类) -->
|
||||||
<div class="zone left">
|
<div class="zone left"></div>
|
||||||
<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)"
|
|
||||||
>
|
|
||||||
<component :is="t.icon" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中:画布尺寸(预设 + 比例锁 + 自定义宽高) -->
|
<!-- 中:工具组 -->
|
||||||
<div class="zone mid">
|
<div class="zone mid">
|
||||||
<select
|
<Dropdown :min-width="240">
|
||||||
class="preset"
|
<template #trigger="{ open, toggle }">
|
||||||
:value="presetId"
|
<button
|
||||||
:title="presetTitle"
|
class="tbtn"
|
||||||
aria-label="画布尺寸预设"
|
:class="{ active: open }"
|
||||||
@change="pickPreset(($event.target as HTMLSelectElement).value)"
|
title="画布编辑"
|
||||||
>
|
aria-label="画布编辑"
|
||||||
<option v-for="p in SIZE_PRESETS" :key="p.id" :value="p.id" :title="`${p.width}×${p.height}px`">
|
:aria-expanded="open"
|
||||||
{{ shortPresetLabel(p.label) }}
|
@click="toggle"
|
||||||
</option>
|
>
|
||||||
<option value="custom">自定义</option>
|
<IconCanvasEdit />
|
||||||
</select>
|
<IconChevronDown class="caret" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #panel="{ close }">
|
||||||
|
<CanvasMenu :close="close" />
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
<div class="dims">
|
<button
|
||||||
<input
|
class="tbtn"
|
||||||
type="number"
|
:class="{ active: tool === 'select' }"
|
||||||
min="16"
|
:title="tip('toolSelect', '选择工具')"
|
||||||
class="num"
|
aria-label="选择工具"
|
||||||
:value="size.width"
|
@click="runCommand('toolSelect')"
|
||||||
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">
|
<IconSelect />
|
||||||
{{ shortPresetLabel(r.label) }}
|
</button>
|
||||||
</option>
|
|
||||||
</select>
|
<button
|
||||||
|
class="tbtn"
|
||||||
|
:class="{ active: tool === 'text' }"
|
||||||
|
:title="tip('toolText', '文字工具')"
|
||||||
|
aria-label="文字工具"
|
||||||
|
@click="runCommand('toolText')"
|
||||||
|
>
|
||||||
|
<IconTextTool />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Dropdown :min-width="200">
|
||||||
|
<template #trigger="{ open, toggle }">
|
||||||
|
<span class="combo">
|
||||||
|
<button
|
||||||
|
class="tbtn combo-body"
|
||||||
|
:class="{ active: tool === 'shape' }"
|
||||||
|
:title="tip('toolShape', '形状工具')"
|
||||||
|
aria-label="形状工具"
|
||||||
|
@click="pickShapeTool(open, toggle)"
|
||||||
|
>
|
||||||
|
<component :is="shapeIcon" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tbtn combo-caret"
|
||||||
|
:class="{ active: open }"
|
||||||
|
title="形状类型"
|
||||||
|
aria-label="选择形状类型"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<IconChevronDown />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #panel>
|
||||||
|
<ShapeMenu />
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Dropdown :min-width="200">
|
||||||
|
<template #trigger="{ open, toggle }">
|
||||||
|
<span class="combo">
|
||||||
|
<button
|
||||||
|
class="tbtn combo-body"
|
||||||
|
:class="{ active: tool === 'background' }"
|
||||||
|
:title="tip('toolBackground', '背景工具')"
|
||||||
|
aria-label="背景工具"
|
||||||
|
@click="pickBackgroundTool(open, toggle)"
|
||||||
|
>
|
||||||
|
<IconBackgroundTool />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tbtn combo-caret"
|
||||||
|
:class="{ active: open }"
|
||||||
|
title="背景来源"
|
||||||
|
aria-label="选择背景来源"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<IconChevronDown />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #panel>
|
||||||
|
<BackgroundMenu />
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右:撤销 / 重做 / 导出 -->
|
<!-- 右:撤销 / 重做 / 导出 -->
|
||||||
@@ -263,6 +271,7 @@ async function scrollToExport() {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -289,36 +298,34 @@ async function scrollToExport() {
|
|||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 画布编辑按钮里的下角标,比工具图标小一档。 */
|
||||||
|
.tbtn .caret {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 形状 / 背景的「本体 + 角标」组合 ---- */
|
||||||
|
.combo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.combo .tbtn {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.combo .combo-body {
|
||||||
|
border-radius: var(--radius) 0 0 var(--radius);
|
||||||
|
}
|
||||||
|
.combo .combo-caret {
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
|
}
|
||||||
|
.combo-caret .icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.vr {
|
.vr {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: var(--line);
|
background: var(--line);
|
||||||
margin: 0 6px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,147 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/** 【占位】CanvasMenu 的内容由对应开发线实现。见任务书。 */
|
/**
|
||||||
|
* 画布编辑下拉:预设 / 分辨率 / 比例。
|
||||||
|
*
|
||||||
|
* 逻辑照搬旧 TopBar 中间那组,不另起一套:尺寸写回一律 `useEditor().setSize()`,
|
||||||
|
* 预设表来自 `@/editor/canvasSize` 的 `SIZE_PRESETS`,比例锁用 `applyRatio`。
|
||||||
|
* `size` 是响应式真值——模板加载、别的面板改尺寸都会反映到这里,所以下拉里的
|
||||||
|
* 显示和命中预设都 watch 真值,而不是只在打开时读一次。
|
||||||
|
*/
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useEditor } from "@/editor/useEditor";
|
||||||
|
import { RATIOS, SIZE_PRESETS, applyRatio } from "@/editor/canvasSize";
|
||||||
|
import { IconCheck } from "@/ui/icons";
|
||||||
|
|
||||||
|
/** 选中一行后要顺手关面板,由 TopBar 通过插槽的 `close` 传进来。 */
|
||||||
|
const props = defineProps<{ close: () => void }>();
|
||||||
|
|
||||||
|
const { size, setSize } = useEditor();
|
||||||
|
|
||||||
|
/** 选中预设后,任何一边宽高的改动都会把它弹回「自定义」。 */
|
||||||
|
const presetId = ref<string>("custom");
|
||||||
|
/** 比例锁。改宽高时另一侧按这个比例联动;"free"(0)表示不锁。 */
|
||||||
|
const ratioId = ref<string>("free");
|
||||||
|
|
||||||
|
const ratio = computed(() => RATIOS.find((r) => r.id === ratioId.value)?.value ?? 0);
|
||||||
|
|
||||||
|
/** 尺寸可能被别处改掉(模板加载、别的面板),要跟着真值走。 */
|
||||||
|
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 });
|
||||||
|
props.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 比例锁只记当前锁定,不立刻改尺寸——改宽高时才用 applyRatio 联动。 */
|
||||||
|
function pickRatio(id: string) {
|
||||||
|
ratioId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动改宽/高。带比例锁时另一侧按 applyRatio 联动。 */
|
||||||
|
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));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="menu-block">(CanvasMenu 待实现)</div>
|
<div class="canvas-menu">
|
||||||
|
<div class="menu-title">预设</div>
|
||||||
|
<button
|
||||||
|
v-for="p in SIZE_PRESETS"
|
||||||
|
:key="p.id"
|
||||||
|
class="menu-row"
|
||||||
|
:class="{ on: presetId === p.id }"
|
||||||
|
@click="pickPreset(p.id)"
|
||||||
|
>
|
||||||
|
<span class="name">{{ p.label }}</span>
|
||||||
|
<span class="menu-aside">{{ p.width }}×{{ p.height }}</span>
|
||||||
|
<IconCheck v-if="presetId === p.id" class="check" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="menu-sep"></div>
|
||||||
|
|
||||||
|
<div class="menu-title">分辨率</div>
|
||||||
|
<div class="menu-block">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-sep"></div>
|
||||||
|
|
||||||
|
<div class="menu-title">比例</div>
|
||||||
|
<button
|
||||||
|
v-for="r in RATIOS"
|
||||||
|
:key="r.id"
|
||||||
|
class="menu-row"
|
||||||
|
:class="{ on: ratioId === r.id }"
|
||||||
|
title="锁定比例:改宽高时另一侧按此联动"
|
||||||
|
@click="pickRatio(r.id)"
|
||||||
|
>
|
||||||
|
<span class="name">{{ r.label }}</span>
|
||||||
|
<IconCheck v-if="ratioId === r.id" class="check" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 名字占满剩余空间,把右侧的尺寸 / 勾推到行尾。 */
|
||||||
|
.name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中行的勾。图标走 currentColor,`.menu-row.on` 已把颜色置成 accent。 */
|
||||||
|
.check {
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dims {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.times {
|
||||||
|
color: var(--fg-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user