feat: 右栏只显示与选中对象相关的内容
滤镜 / 文字两组改成选中对应类型才出现,去掉「请先选中图片」那种占位提示—— 一行提示的高度和一组内容差不多,等于白占位置。导出与模板常驻(与选中无关, 且顶栏导出按钮要滚到它)。背景组从右栏摘掉,挪进顶栏的背景工具下拉。 Inspector 在没选中时只留一句说明,画板尺寸的只读回显去掉——尺寸改到顶栏的 画布编辑下拉里了。 补 body[data-tool] 的画布光标规则:文字工具是 I 形,形状工具是十字。
This commit is contained in:
@@ -4,7 +4,7 @@ import { ActiveSelection, Rect } from "fabric";
|
||||
import type { FabricObject } from "fabric";
|
||||
import { useEditor, layerKindOf } from "@/editor/useEditor";
|
||||
import { commit } from "@/editor/useHistory";
|
||||
import type { TransformValues } from "@/types";
|
||||
import type { LayerKind, TransformValues } from "@/types";
|
||||
import { IconAngle, IconRadius, IconOpacity, IconLinked, IconUnlinked } from "@/ui/icons";
|
||||
|
||||
/**
|
||||
@@ -21,6 +21,10 @@ import { IconAngle, IconRadius, IconOpacity, IconLinked, IconUnlinked } from "@/
|
||||
* 版式照 Figma 的设计面板收窄:没有分组标题,字段是「图标/字母 + 无边框输入框」
|
||||
* 的胶囊,两列一行铺开。右栏总高有限,标题行越少留给下面工具分组的越多。
|
||||
*
|
||||
* **没选中任何东西时这里只有一句提示**(用户 2026-09-05 的要求:右栏只显示
|
||||
* 当前选中对象相关的东西)。画板尺寸挪去顶栏的「画布编辑」下拉了,
|
||||
* 不再在这里做只读回显。
|
||||
*
|
||||
* 两条容易做错的语义(任务书点名,见文件末尾的纯函数区):
|
||||
* 1. W/H 是缩放后的**实际占位尺寸** = 原始尺寸 × scale,不是 `getScaledWidth()`——
|
||||
* 后者会把 `strokeWidth`(默认 1,即使 stroke 为 null)也算进去,导致
|
||||
@@ -30,7 +34,7 @@ import { IconAngle, IconRadius, IconOpacity, IconLinked, IconUnlinked } from "@/
|
||||
* 要 `left - width*scale/2` 换算,不能直接拿 `obj.left`。
|
||||
*/
|
||||
|
||||
const { activeObject, revision, size, getCanvas, bumpRevision } = useEditor();
|
||||
const { activeObject, revision, getCanvas, bumpRevision } = useEditor();
|
||||
|
||||
/** 当前选中的单个对象。多选(ActiveSelection)或没选时返回 null。 */
|
||||
function singleObject(): FabricObject | null {
|
||||
@@ -77,13 +81,22 @@ const values = computed<TransformValues | null>(() => {
|
||||
const headline = computed(() => {
|
||||
void revision.value;
|
||||
const obj = singleObject();
|
||||
if (obj) {
|
||||
const kind = layerKindOf(obj);
|
||||
return kind === "image" ? "图片" : kind === "text" ? "文字" : kind === "rect" ? "矩形" : "对象";
|
||||
}
|
||||
return activeObject.value ? "多选" : "画板";
|
||||
if (obj) return KIND_LABEL[layerKindOf(obj)];
|
||||
return activeObject.value ? "多选" : "";
|
||||
});
|
||||
|
||||
const KIND_LABEL: Record<LayerKind, string> = {
|
||||
background: "背景",
|
||||
image: "图片",
|
||||
text: "文字",
|
||||
rect: "矩形",
|
||||
line: "直线",
|
||||
arrow: "箭头",
|
||||
ellipse: "圆形",
|
||||
polygon: "多边形",
|
||||
other: "对象",
|
||||
};
|
||||
|
||||
/**
|
||||
* 宽高比例锁(Figma 设计面板 W/H 之间那个链条)。
|
||||
*
|
||||
@@ -292,7 +305,7 @@ function normalizeAngle(angle: number): number {
|
||||
|
||||
<template>
|
||||
<aside class="inspector">
|
||||
<div class="head">
|
||||
<div v-if="values || activeObject" class="head">
|
||||
<span class="head-kind">{{ headline }}</span>
|
||||
</div>
|
||||
|
||||
@@ -372,20 +385,8 @@ function normalizeAngle(angle: number): number {
|
||||
<!-- 多选:不做几何编辑 -->
|
||||
<p v-else-if="activeObject" class="meta">已选中多个对象。几何属性只在单选时编辑。</p>
|
||||
|
||||
<!-- 没选中:画板尺寸(只读,改尺寸在顶栏) -->
|
||||
<template v-else>
|
||||
<div class="grid">
|
||||
<label class="f" title="画板宽度(在顶栏修改)">
|
||||
<span class="k">W</span>
|
||||
<input type="number" disabled :value="size.width" />
|
||||
</label>
|
||||
<label class="f" title="画板高度(在顶栏修改)">
|
||||
<span class="k">H</span>
|
||||
<input type="number" disabled :value="size.height" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="meta">画板尺寸在顶栏修改。选中画布上的对象,这里会变成它的几何属性。</p>
|
||||
</template>
|
||||
<!-- 没选中:右栏留空,只留一句话说明它什么时候有内容 -->
|
||||
<p v-else class="meta">选中画布上的对象后,这里显示它的位置、尺寸与外观。</p>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from "vue";
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import SourcePanel from "./panels/SourcePanel.vue";
|
||||
import FilterPanel from "./panels/FilterPanel.vue";
|
||||
import TextPanel from "./panels/TextPanel.vue";
|
||||
import ExportPanel from "./panels/ExportPanel.vue";
|
||||
import {
|
||||
IconImage,
|
||||
IconFilter,
|
||||
IconText,
|
||||
IconExport,
|
||||
@@ -16,44 +14,44 @@ import {
|
||||
import { toolGroupRequest, type ToolGroupId } from "@/ui/toolGroups";
|
||||
|
||||
/**
|
||||
* 右栏下半:背景 / 滤镜 / 文字 / 导出与模板 四个工具面板的容器。
|
||||
* 右栏下半:滤镜 / 文字 / 导出与模板。
|
||||
*
|
||||
* 版式照 Figma 的设计面板:每组平时只是**一行 32px 的标题**(图标 + 名字 + 右侧
|
||||
* 加号),点开才展开内容。**默认四组全收**——这四个面板加起来一千多行控件,
|
||||
* 常驻展开的话右栏一屏塞不下,找任何一个都得先滚。
|
||||
* 加号),点开才展开内容。
|
||||
*
|
||||
* 自动展开只发生在一种情况:选中的对象类型正好对应某一组(选图片 → 滤镜,
|
||||
* 选文字 → 文字)。这是「我要改这个」的明确信号,替用户省一次点击;反过来
|
||||
* 不自动收起,收起是用户自己的事。
|
||||
* **一组只在它作用得上的时候才出现**(用户 2026-09-05 的要求:右栏只显示
|
||||
* 当前选中对象相关的东西)。选中图片才有「滤镜」,选中文字才有「文字」;
|
||||
* 以前那种「组在、但里面写一行『请先选中图片』」的做法去掉了——一行提示
|
||||
* 占的高度和一组内容差不多,等于白占位置。
|
||||
*
|
||||
* 面板组件用 v-show 而不是 v-if 隐藏,保持挂载,它们内部 watch activeObject
|
||||
* 的状态才能一直跟着画布走。滤镜和文字组在条件不满足时显示一行提示。
|
||||
* 「导出与模板」是例外,常驻:它跟选中了什么无关,而且顶栏的导出按钮要能
|
||||
* 滚到它。
|
||||
*
|
||||
* 「背景」组不在这里了,2026-09-05 挪进顶栏的背景工具下拉(`BackgroundMenu`),
|
||||
* 因为背景是**那一个专用背景图层**,不是选中某个对象后才编辑的属性。
|
||||
*
|
||||
* 隐藏用 v-show 不用 v-if:面板组件保持挂载,它们内部 watch activeObject
|
||||
* 的状态才能一直跟着画布走,重新出现时不用从头初始化。
|
||||
*
|
||||
* 别处要把用户送到某个面板,走 `@/ui/toolGroups` 的 `openToolGroup(id)`。
|
||||
*/
|
||||
const { selectionKind } = useEditor();
|
||||
|
||||
const GROUPS = [
|
||||
{ id: "bg", label: "背景", icon: IconImage, scope: "" },
|
||||
{ id: "filter", label: "滤镜", icon: IconFilter, scope: "图片" },
|
||||
{ id: "text", label: "文字", icon: IconText, scope: "文字" },
|
||||
{ id: "export", label: "导出与模板", icon: IconExport, scope: "" },
|
||||
] as const;
|
||||
|
||||
/** 每组是否展开。纯 UI 状态;组的内容是否可用看 selectionKind。 */
|
||||
const open = reactive<Record<ToolGroupId, boolean>>({
|
||||
bg: false,
|
||||
filter: false,
|
||||
text: false,
|
||||
export: false,
|
||||
});
|
||||
|
||||
const isImage = computed(() => selectionKind.value === "image");
|
||||
const isText = computed(() => selectionKind.value === "text");
|
||||
|
||||
watch(selectionKind, (kind) => {
|
||||
if (kind === "image") open.filter = true;
|
||||
else if (kind === "text") open.text = true;
|
||||
/**
|
||||
* 每组是否展开。
|
||||
*
|
||||
* 滤镜和文字默认展开:它们只在选中对应对象时才出现,出现本身就说明用户
|
||||
* 正要改它,再让人多点一下没有道理。导出默认收起——它常驻,展开着会一直
|
||||
* 占掉右栏下半屏。
|
||||
*/
|
||||
const open = reactive<Record<ToolGroupId, boolean>>({
|
||||
bg: false,
|
||||
filter: true,
|
||||
text: true,
|
||||
export: false,
|
||||
});
|
||||
|
||||
/** 别处(顶栏的导出按钮)点名要展开某一组。 */
|
||||
@@ -68,33 +66,39 @@ function toggle(id: ToolGroupId) {
|
||||
|
||||
<template>
|
||||
<div class="tool-panels">
|
||||
<section v-for="g in GROUPS" :key="g.id" class="sec" :class="{ open: open[g.id] }">
|
||||
<button type="button" class="head" :aria-expanded="open[g.id]" @click="toggle(g.id)">
|
||||
<component :is="g.icon" class="head-icon" />
|
||||
<span class="title">{{ g.label }}</span>
|
||||
<span v-if="g.scope && !open[g.id]" class="scope">{{ g.scope }}</span>
|
||||
<IconChevronDown v-if="open[g.id]" class="mark" />
|
||||
<section v-show="isImage" class="sec" :class="{ open: open.filter }">
|
||||
<button type="button" class="head" :aria-expanded="open.filter" @click="toggle('filter')">
|
||||
<IconFilter class="head-icon" />
|
||||
<span class="title">滤镜</span>
|
||||
<IconChevronDown v-if="open.filter" class="mark" />
|
||||
<IconPlus v-else class="mark" />
|
||||
</button>
|
||||
<div v-show="open.filter" class="body">
|
||||
<FilterPanel />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-show="open[g.id]" class="body">
|
||||
<template v-if="g.id === 'bg'">
|
||||
<SourcePanel />
|
||||
</template>
|
||||
<section v-show="isText" class="sec" :class="{ open: open.text }">
|
||||
<button type="button" class="head" :aria-expanded="open.text" @click="toggle('text')">
|
||||
<IconText class="head-icon" />
|
||||
<span class="title">文字</span>
|
||||
<IconChevronDown v-if="open.text" class="mark" />
|
||||
<IconPlus v-else class="mark" />
|
||||
</button>
|
||||
<div v-show="open.text" class="body">
|
||||
<TextPanel />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-else-if="g.id === 'filter'">
|
||||
<FilterPanel v-show="isImage" />
|
||||
<p v-if="!isImage" class="hint">选中画布上的一张图片,这里会列出可叠加的滤镜。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="g.id === 'text'">
|
||||
<TextPanel v-show="isText" />
|
||||
<p v-if="!isText" class="hint">用文字工具(T)新建,或选中已有文字,再到这里调字体样式。</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<ExportPanel />
|
||||
</template>
|
||||
<section class="sec" :class="{ open: open.export }">
|
||||
<button type="button" class="head" :aria-expanded="open.export" @click="toggle('export')">
|
||||
<IconExport class="head-icon" />
|
||||
<span class="title">导出与模板</span>
|
||||
<IconChevronDown v-if="open.export" class="mark" />
|
||||
<IconPlus v-else class="mark" />
|
||||
</button>
|
||||
<div v-show="open.export" class="body">
|
||||
<ExportPanel />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -145,24 +149,12 @@ function toggle(id: ToolGroupId) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 收起时在右边提示这组作用在什么上;展开后收掉,让位给内容。 */
|
||||
.scope {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
color: var(--fg-dim);
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 展开/收起的记号:收起是加号,展开是朝下的角标(和 Figma 一致)。 */
|
||||
.mark {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.scope + .mark {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.head:hover .mark {
|
||||
color: var(--fg);
|
||||
}
|
||||
@@ -170,11 +162,4 @@ function toggle(id: ToolGroupId) {
|
||||
.body {
|
||||
padding: 2px 10px 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 2px 0 0;
|
||||
color: var(--fg-dim);
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -157,3 +157,16 @@ label {
|
||||
.menu-block {
|
||||
padding: 4px 9px 8px;
|
||||
}
|
||||
|
||||
/* ================================================================ 工具光标
|
||||
* 当前工具决定画布区的鼠标指针。工具状态在 `useEditor().tool`,由 TopBar
|
||||
* 写到 `document.body.dataset.tool`——这里必须是全局规则:`.stage` 在
|
||||
* Workspace.vue 里,body 在它外面,作用域样式跨不过去。
|
||||
*/
|
||||
body[data-tool="text"] .stage {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
body[data-tool="shape"] .stage {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user