chore: 本轮改造的共享契约与占位——ToolId/ShapeKind/LayerKind 扩充、Dropdown 壳、形状表、三个菜单与两个 composable 的桩
This commit is contained in:
@@ -35,10 +35,11 @@ import { computed, nextTick, onBeforeUnmount, ref } from "vue";
|
||||
import type { LayerInfo, LayerKind } from "@/types";
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import { useLayers } from "@/editor/useLayers";
|
||||
import { shapeMeta } from "@/editor/shapes";
|
||||
import {
|
||||
IconImage,
|
||||
IconText,
|
||||
IconRect,
|
||||
IconBackgroundTool,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconLocked,
|
||||
@@ -65,11 +66,16 @@ const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v
|
||||
|
||||
/* ------------------------------------------------ 图标 */
|
||||
|
||||
/** 行首的类型图标。`other` 兜底用「多层」那个,表示不属于以上三类的对象。 */
|
||||
/** 行首的类型图标。形状五种直接借形状表里的那份,和工具栏图标保持一致。 */
|
||||
const KIND_ICON: Record<LayerKind, unknown> = {
|
||||
background: IconBackgroundTool,
|
||||
image: IconImage,
|
||||
text: IconText,
|
||||
rect: IconRect,
|
||||
rect: shapeMeta("rect").icon,
|
||||
line: shapeMeta("line").icon,
|
||||
arrow: shapeMeta("arrow").icon,
|
||||
ellipse: shapeMeta("ellipse").icon,
|
||||
polygon: shapeMeta("polygon").icon,
|
||||
other: IconTemplate,
|
||||
};
|
||||
|
||||
|
||||
@@ -48,15 +48,15 @@ const { canUndo, canRedo } = useHistory();
|
||||
const TOOLS = [
|
||||
{ cmd: "toolSelect", label: "选择工具", icon: IconSelect },
|
||||
{ cmd: "toolText", label: "文字工具", icon: IconText },
|
||||
{ cmd: "toolRect", label: "矩形工具", icon: IconRect },
|
||||
{ cmd: "toolImage", label: "图片工具", icon: IconImage },
|
||||
{ cmd: "toolShape", label: "形状工具", icon: IconRect },
|
||||
{ cmd: "toolBackground", label: "背景工具", icon: IconImage },
|
||||
] as const;
|
||||
|
||||
const TOOL_OF: Record<string, ToolId> = {
|
||||
toolSelect: "select",
|
||||
toolText: "text",
|
||||
toolRect: "rect",
|
||||
toolImage: "image",
|
||||
toolShape: "shape",
|
||||
toolBackground: "background",
|
||||
};
|
||||
|
||||
function toolOf(cmd: string): ToolId {
|
||||
|
||||
@@ -93,8 +93,8 @@ function onPointerDown(e: PointerEvent) {
|
||||
const scene = c.getScenePoint(e);
|
||||
void p;
|
||||
if (tool.value === "text") addText();
|
||||
else if (tool.value === "rect") addRect({ x: scene.x, y: scene.y });
|
||||
if (tool.value !== "image") {
|
||||
else if (tool.value === "shape") addRect({ x: scene.x, y: scene.y });
|
||||
if (tool.value !== "background") {
|
||||
tool.value = "select";
|
||||
commit();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/** 【占位】BackgroundMenu 的内容由对应开发线实现。见任务书。 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-block">(BackgroundMenu 待实现)</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/** 【占位】CanvasMenu 的内容由对应开发线实现。见任务书。 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-block">(CanvasMenu 待实现)</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/** 【占位】ShapeMenu 的内容由对应开发线实现。见任务书。 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-block">(ShapeMenu 待实现)</div>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
/**
|
||||
* 顶栏三个下拉(画布编辑 / 形状 / 背景)共用的浮层壳。
|
||||
*
|
||||
* 它只管四件事:开合、点外面关掉、Esc 关掉、贴着触发器定位并在撞到窗口右边缘时
|
||||
* 翻到左对齐。菜单**内容**由使用方通过 `panel` 插槽给,行的样式用 style.css 里的
|
||||
* 全局 `.menu-*` 类(作用域样式够不到插槽内容,所以那几个类必须是全局的)。
|
||||
*
|
||||
* 触发器也由使用方给(`trigger` 插槽),插槽参数带 `open` 和 `toggle`,
|
||||
* 这样按钮的高亮态和点击行为都由使用方决定,壳不猜。
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 面板最小宽度 px。 */
|
||||
minWidth?: number;
|
||||
/** 面板相对触发器的水平对齐。撞到窗口边缘时会自动翻到另一边。 */
|
||||
align?: "left" | "center";
|
||||
}>(),
|
||||
{ minWidth: 200, align: "left" },
|
||||
);
|
||||
|
||||
const open = ref(false);
|
||||
const rootEl = ref<HTMLElement | null>(null);
|
||||
const panelEl = ref<HTMLElement | null>(null);
|
||||
/** 面板相对 root(触发器)的左偏移 px。 */
|
||||
const offsetX = ref(0);
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value;
|
||||
}
|
||||
|
||||
function close() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ close });
|
||||
|
||||
/**
|
||||
* 打开后量一次,撞到窗口右边缘就往左挪。
|
||||
* 用 window 而不是父容器做边界:顶栏是 `overflow: hidden` 的,但面板挂在
|
||||
* 顶栏里靠 `position: absolute` 浮出去,真正会裁掉它的是浏览器窗口。
|
||||
*/
|
||||
watch(open, async (val) => {
|
||||
if (!val) return;
|
||||
offsetX.value = 0;
|
||||
await nextTick();
|
||||
const root = rootEl.value;
|
||||
const panel = panelEl.value;
|
||||
if (!root || !panel) return;
|
||||
const r = root.getBoundingClientRect();
|
||||
const pw = panel.offsetWidth;
|
||||
let x = props.align === "center" ? (r.width - pw) / 2 : 0;
|
||||
const MARGIN = 8;
|
||||
const leftAbs = r.left + x;
|
||||
if (leftAbs + pw > window.innerWidth - MARGIN) x -= leftAbs + pw - (window.innerWidth - MARGIN);
|
||||
if (r.left + x < MARGIN) x = MARGIN - r.left;
|
||||
offsetX.value = x;
|
||||
});
|
||||
|
||||
function onDocPointerDown(e: PointerEvent) {
|
||||
if (!open.value) return;
|
||||
if (rootEl.value && !rootEl.value.contains(e.target as Node)) close();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && open.value) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("pointerdown", onDocPointerDown);
|
||||
// 捕获阶段:面板里有输入框时,Esc 要先关面板,不让全局快捷键抢走。
|
||||
window.addEventListener("keydown", onKeyDown, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("pointerdown", onDocPointerDown);
|
||||
window.removeEventListener("keydown", onKeyDown, true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rootEl" class="dd">
|
||||
<slot name="trigger" :open="open" :toggle="toggle" />
|
||||
|
||||
<div
|
||||
v-if="open"
|
||||
ref="panelEl"
|
||||
class="dd-panel"
|
||||
:style="{ left: offsetX + 'px', minWidth: minWidth + 'px' }"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<slot name="panel" :close="close" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dd {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dd-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 40;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--bg3);
|
||||
box-shadow: 0 10px 34px rgb(0 0 0 / 0.55);
|
||||
/* 顶栏很矮,面板可能比它长得多,超出就自己滚。 */
|
||||
max-height: calc(100vh - var(--topbar-h) - 24px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user