图标统一走 unplugin-icons + hugeicons,构建期内联,出口收在 src/ui/icons.ts。 顺带修掉 TopBar 那个 undo 图标缺弧命令的 path。 右栏下半四组默认全收,收起态只有一行 32px;顶栏导出按钮改成先展开 「导出与模板」再滚,否则滚到底只看得到一行标题。 属性区去掉分组标题,改成 Figma 那种「字母/图标 + 无边框输入框」两列密排, 新增 W/H 宽高比锁(比例在开锁那一刻定格,不每次编辑现算)。 删掉 Sidebar.vue 和 panels/CanvasPanel.vue:上一轮改 Figma 布局之后 它们已无人 import,--sidebar-w 同批遗留。
181 lines
5.0 KiB
Vue
181 lines
5.0 KiB
Vue
<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,
|
||
IconPlus,
|
||
IconChevronDown,
|
||
} from "@/ui/icons";
|
||
import { toolGroupRequest, type ToolGroupId } from "@/ui/toolGroups";
|
||
|
||
/**
|
||
* 右栏下半:背景 / 滤镜 / 文字 / 导出与模板 四个工具面板的容器。
|
||
*
|
||
* 版式照 Figma 的设计面板:每组平时只是**一行 32px 的标题**(图标 + 名字 + 右侧
|
||
* 加号),点开才展开内容。**默认四组全收**——这四个面板加起来一千多行控件,
|
||
* 常驻展开的话右栏一屏塞不下,找任何一个都得先滚。
|
||
*
|
||
* 自动展开只发生在一种情况:选中的对象类型正好对应某一组(选图片 → 滤镜,
|
||
* 选文字 → 文字)。这是「我要改这个」的明确信号,替用户省一次点击;反过来
|
||
* 不自动收起,收起是用户自己的事。
|
||
*
|
||
* 面板组件用 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;
|
||
});
|
||
|
||
/** 别处(顶栏的导出按钮)点名要展开某一组。 */
|
||
watch(toolGroupRequest, (req) => {
|
||
if (req) open[req.id] = true;
|
||
});
|
||
|
||
function toggle(id: ToolGroupId) {
|
||
open[id] = !open[id];
|
||
}
|
||
</script>
|
||
|
||
<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" />
|
||
<IconPlus v-else class="mark" />
|
||
</button>
|
||
|
||
<div v-show="open[g.id]" class="body">
|
||
<template v-if="g.id === 'bg'">
|
||
<SourcePanel />
|
||
</template>
|
||
|
||
<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>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.tool-panels {
|
||
padding-bottom: 18px;
|
||
}
|
||
|
||
.sec {
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
|
||
/* 标题行:整行可点,覆盖全局 button 外观。收起态就是这一行的全部高度。 */
|
||
.head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
height: 32px;
|
||
padding: 0 10px;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
color: var(--fg-dim);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
}
|
||
.head:hover {
|
||
background: var(--bg3);
|
||
color: var(--fg);
|
||
}
|
||
.head:focus-visible {
|
||
outline: 1px solid var(--accent);
|
||
outline-offset: -1px;
|
||
}
|
||
.sec.open .head {
|
||
color: var(--fg);
|
||
}
|
||
|
||
.head-icon {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.title {
|
||
font-size: 12px;
|
||
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);
|
||
}
|
||
|
||
.body {
|
||
padding: 2px 10px 12px;
|
||
}
|
||
|
||
.hint {
|
||
margin: 2px 0 0;
|
||
color: var(--fg-dim);
|
||
font-size: 11px;
|
||
line-height: 1.7;
|
||
}
|
||
</style>
|