Files
cover-studio/src/components/ToolPanels.vue
T
im0o c9803827fd T10: 工具面板收进右栏折叠分组,拖放图片改为新增图层
- ToolPanels:背景/滤镜/文字/导出与模板四组可折叠分组,选中图片/文字自动展开相关组
- SourcePanel:拖放与文件选择默认作为新图层加入(addImage toBack:false 并选中),
  「作为背景铺满」改为面板里的显式勾选项,不再是一切的默认行为;多张可一次全加
- 上传 object URL 改为会话内保留、卸载时统一释放(撤销与模板重载要按 src 重新取图)
- 预设几何与 Unsplash 保持原「铺背景」语义,不受勾选项影响
- 新增 pickImageFiles 纯函数与 src/sources/selfcheck.ts Node 侧自检
2026-09-05 02:34:22 +08:00

214 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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";
/**
* 【T10 — 工具面板的重新安置】
*
* 旧版把所有东西塞在一个右侧栏里。改成 Figma 式布局之后,那四个面板
* (背景 / 滤镜 / 文字 / 导出与模板)没有任何挂载点——功能不可达。这里
* 把它们收成**可折叠的分组**,占右栏下半层(App.vue 已挂好,不动布局):
*
* | 组 | 来源 | 内容何时可用 |
* |----------|---------------|---------------------------|
* | 背景 | SourcePanel | 常驻 |
* | 滤镜 | FilterPanel | 选中图片时 |
* | 文字 | TextPanel | 选中文字时 |
* | 导出与模板| ExportPanel | 常驻 |
*
* 规则:
* - 分组标题可手动折叠(展开与否只是 UI 状态,不销毁面板)。
* - **选中对象自动展开相关的那组**:选中图片自动展开「滤镜」,
* 选中文字自动展开「文字」。
* - 内容是否可用看 `selectionKind`(filter 要图片、text 要文字)。
* 面板组件**保持挂载**(用 v-show 而不是 v-if 隐藏),这样它们内部
* watch activeObject 的状态能一直跟随画布;条件不满足时组内显示一行提示。
*/
const { selectionKind } = useEditor();
type GroupId = "bg" | "filter" | "text" | "export";
/** 每组是否展开。展开与否是纯 UI 状态;组的内容是否可用看 selectionKind。 */
const open = reactive<Record<GroupId, boolean>>({
bg: true,
filter: false,
text: false,
export: true,
});
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;
});
function toggle(id: GroupId) {
open[id] = !open[id];
}
</script>
<template>
<div class="tool-panels">
<!-- ================= 背景 ================= -->
<section class="tp-sec" :class="{ open: open.bg }">
<button
type="button"
class="tp-head"
:aria-expanded="open.bg"
@click="toggle('bg')"
>
<span class="chev" aria-hidden="true"></span>
<span class="tp-title">背景</span>
</button>
<div v-show="open.bg" class="tp-body">
<SourcePanel />
</div>
</section>
<!-- ================= 滤镜(选中图片时可用) ================= -->
<section class="tp-sec" :class="{ open: open.filter }">
<button
type="button"
class="tp-head"
:aria-expanded="open.filter"
@click="toggle('filter')"
>
<span class="chev" aria-hidden="true"></span>
<span class="tp-title">滤镜</span>
<span class="tp-scope">作用于图片</span>
</button>
<div v-show="open.filter" class="tp-body">
<FilterPanel v-show="isImage" />
<p v-if="!isImage" class="tp-hint">
在画布上选中一张图片后,这一组会自动展开,列出可叠加的滤镜。
</p>
</div>
</section>
<!-- ================= 文字(选中文字时可用) ================= -->
<section class="tp-sec" :class="{ open: open.text }">
<button
type="button"
class="tp-head"
:aria-expanded="open.text"
@click="toggle('text')"
>
<span class="chev" aria-hidden="true"></span>
<span class="tp-title">文字</span>
<span class="tp-scope">作用于文字</span>
</button>
<div v-show="open.text" class="tp-body">
<TextPanel v-show="isText" />
<p v-if="!isText" class="tp-hint">
用顶栏文字工具(T)添加文字,或选中画布上已有的文字,再到这里调字体样式。
</p>
</div>
</section>
<!-- ================= 导出与模板 ================= -->
<section class="tp-sec" :class="{ open: open.export }">
<button
type="button"
class="tp-head"
:aria-expanded="open.export"
@click="toggle('export')"
>
<span class="chev" aria-hidden="true"></span>
<span class="tp-title">导出与模板</span>
</button>
<div v-show="open.export" class="tp-body">
<ExportPanel />
</div>
</section>
</div>
</template>
<style scoped>
.tool-panels {
padding: 2px 0 18px;
}
/* 分组之间一条细分隔线,整体顶到右栏上边缘 */
.tp-sec {
border-top: 1px solid var(--line);
}
.tp-sec:first-child {
border-top: none;
}
/* 分组标题:整行可点,覆盖全局 button 外观 */
.tp-head {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 10px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
cursor: pointer;
text-align: left;
}
.tp-head:hover {
background: var(--bg3);
border-color: transparent;
}
.tp-head:focus-visible {
outline: 1px solid var(--accent);
outline-offset: -1px;
}
/* 展开/收起的三角,收拢朝右、展开朝下 */
.chev {
flex: none;
width: 0;
height: 0;
border-left: 4px solid var(--fg-dim);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
transition: transform 0.12s ease;
}
.tp-sec.open .chev {
transform: rotate(90deg);
}
.tp-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--fg-dim);
}
.tp-sec.open .tp-title {
color: var(--fg);
}
/* 组的适用范围提示,靠右 */
.tp-scope {
margin-left: auto;
font-size: 11px;
color: var(--fg-dim);
opacity: 0.85;
white-space: nowrap;
}
.tp-body {
padding: 0 12px 12px;
}
/* 组展开了但条件不满足(没选图片/文字)时给一行提示,不渲染控制项 */
.tp-hint {
color: var(--fg-dim);
font-size: 12px;
line-height: 1.7;
margin: 2px 0 0;
}
</style>