fix: 全窗口拖放搬出 SourcePanel,提到 App 层
拖放原来挂在 SourcePanel 的 window 监听上,前提是那个面板常驻挂载。右栏改成 按选中显示之后前提不成立,拖图进来会静默失效。抽成 useFileDrop,由 App 挂一次, 和右栏显示什么解耦,并补上拖入时的提示遮罩。
This commit is contained in:
+39
-2
@@ -8,6 +8,7 @@ import BottomDock from "./components/BottomDock.vue";
|
||||
import SelectionMenu from "./components/SelectionMenu.vue";
|
||||
import ContextMenu from "./components/ContextMenu.vue";
|
||||
import { useShortcuts } from "./editor/useShortcuts";
|
||||
import { useFileDrop } from "./editor/useFileDrop";
|
||||
|
||||
/**
|
||||
* Figma 式布局:顶部工具栏 / 左侧图层 / 中间画布 / 右侧属性 / 底部悬浮胶囊。
|
||||
@@ -17,8 +18,12 @@ import { useShortcuts } from "./editor/useShortcuts";
|
||||
* 它们放在 .stage 里而不是 grid 的格子里,就是这个原因。
|
||||
*
|
||||
* **这个文件由主控独占**,并行开发期间各线只实现自己那个组件。
|
||||
*
|
||||
* 全窗口拖放在这里挂一次:它原来长在 SourcePanel 里,靠那个面板常驻挂载才收得到
|
||||
* window 事件;右栏改成按选中显示之后那个前提没了,所以提到 App 这一层。
|
||||
*/
|
||||
useShortcuts();
|
||||
const { dragging, notice } = useFileDrop();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,10 +36,14 @@ useShortcuts();
|
||||
<SelectionMenu />
|
||||
<BottomDock />
|
||||
<ContextMenu />
|
||||
|
||||
<!-- 拖文件进来时的提示遮罩。只是视觉反馈,事件在 window 上收,不靠它。 -->
|
||||
<div v-if="dragging" class="drop-hint">松手把图片加成新图层</div>
|
||||
<p v-if="notice" class="drop-notice">{{ notice }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 右栏分两层:上半是选中对象的属性(T8),下半是背景 / 滤镜 / 文字 / 导出
|
||||
这些工具面板(T10)。分开是为了让两条开发线各占一个文件,互不干扰。 -->
|
||||
<!-- 右栏分两层:上半是选中对象的几何与外观,下半是滤镜 / 文字 / 导出。
|
||||
两层都只显示与当前选中对象相关的内容,没选中就基本是空的。 -->
|
||||
<div class="area-right">
|
||||
<Inspector class="right-top" />
|
||||
<ToolPanels class="right-bottom" />
|
||||
@@ -80,6 +89,34 @@ useShortcuts();
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.drop-hint {
|
||||
position: absolute;
|
||||
inset: 12px;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px dashed var(--accent);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
color: var(--fg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drop-notice {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 64px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--line);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 画布区是浮层的定位上下文:选中菜单、底部胶囊、右键菜单都绝对定位在它里面。 */
|
||||
.area-stage {
|
||||
grid-area: stage;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { pickImageFiles, fromUpload, releaseObjectUrl } from "@/sources/upload";
|
||||
import { useEditor } from "./useEditor";
|
||||
import { commit } from "./useHistory";
|
||||
|
||||
/**
|
||||
* 全窗口拖放:图片文件拖进窗口任意处,作为**新图层**加进画布。
|
||||
*
|
||||
* 这段原来长在 `SourcePanel.vue` 里,靠「面板常驻挂载」才能收到 window 事件。
|
||||
* 2026-09-05 右栏改成「只显示与选中对象相关的内容」之后那个前提没了——
|
||||
* 面板不再常驻,拖放会跟着一起失效。所以搬到这里,由 `App.vue` 挂一次,
|
||||
* 和右栏显示什么彻底解耦。
|
||||
*
|
||||
* 落到原生 `<input type="file">` 上的拖放不抢:那是浏览器自己的行为,
|
||||
* 抢了反而让文件选择框里的拖放不工作。
|
||||
*/
|
||||
export function useFileDrop() {
|
||||
const { addImage } = useEditor();
|
||||
|
||||
/** 是否正有文件悬在窗口上,用来画那层提示遮罩。 */
|
||||
const dragging = ref(false);
|
||||
const busy = ref(false);
|
||||
/** 最近一次拖放的结果,界面上闪一下就够,不做常驻状态。 */
|
||||
const notice = ref("");
|
||||
|
||||
/**
|
||||
* dragenter/dragleave 会在**每个子元素**上各触发一次,
|
||||
* 光看 dragleave 就收遮罩的话,鼠标划过任何一个子元素都会闪。
|
||||
* 用进出计数配平。
|
||||
*/
|
||||
let depth = 0;
|
||||
/** 本会话建过的 object URL,卸载时一次性释放,否则一直占着内存。 */
|
||||
const liveUrls: string[] = [];
|
||||
|
||||
function hasFiles(dt: DataTransfer | null): boolean {
|
||||
return !!dt && dt.types.includes("Files");
|
||||
}
|
||||
|
||||
function onNativeInput(e: Event): boolean {
|
||||
const el = e.target as Element | null;
|
||||
return !!el && !!el.closest?.('input[type="file"]');
|
||||
}
|
||||
|
||||
function onDragEnter(e: DragEvent) {
|
||||
if (onNativeInput(e) || !hasFiles(e.dataTransfer)) return;
|
||||
e.preventDefault();
|
||||
depth++;
|
||||
dragging.value = true;
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) {
|
||||
if (onNativeInput(e) || !hasFiles(e.dataTransfer)) return;
|
||||
// 不 preventDefault 就根本不会有 drop 事件。
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onDragLeave(e: DragEvent) {
|
||||
if (onNativeInput(e) || !hasFiles(e.dataTransfer)) return;
|
||||
depth = Math.max(0, depth - 1);
|
||||
if (depth === 0) dragging.value = false;
|
||||
}
|
||||
|
||||
async function onDrop(e: DragEvent) {
|
||||
depth = 0;
|
||||
dragging.value = false;
|
||||
if (onNativeInput(e) || !hasFiles(e.dataTransfer)) return;
|
||||
e.preventDefault();
|
||||
const files = Array.from(e.dataTransfer?.files ?? []);
|
||||
if (files.length === 0) return;
|
||||
await addFiles(files);
|
||||
}
|
||||
|
||||
/** 一张一张加,个别失败不影响其余。全部加完才记一步历史。 */
|
||||
async function addFiles(files: readonly File[]) {
|
||||
if (busy.value) return;
|
||||
const images = pickImageFiles(files);
|
||||
if (images.length === 0) {
|
||||
notice.value = "拖进来的文件里没有图片";
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
notice.value = "";
|
||||
let ok = 0;
|
||||
const failed: string[] = [];
|
||||
try {
|
||||
for (const file of images) {
|
||||
try {
|
||||
const asset = await fromUpload(file);
|
||||
// 拖进来的图一律是新图层,不铺底——铺底走顶栏的背景工具。
|
||||
await addImage(asset.src, { toBack: false });
|
||||
liveUrls.push(asset.src);
|
||||
ok++;
|
||||
} catch {
|
||||
failed.push(file.name || "未命名文件");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
if (ok > 0) commit();
|
||||
if (failed.length > 0) {
|
||||
notice.value =
|
||||
ok === 0
|
||||
? `这 ${failed.length} 张都加不进去(图片读不出来)`
|
||||
: `${ok} 张已加入,${failed.length} 张失败`;
|
||||
} else if (ok > 1) {
|
||||
notice.value = `已加进 ${ok} 张图片(最新一张在最上层)`;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("dragenter", onDragEnter);
|
||||
window.addEventListener("dragover", onDragOver);
|
||||
window.addEventListener("dragleave", onDragLeave);
|
||||
window.addEventListener("drop", onDrop);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("dragenter", onDragEnter);
|
||||
window.removeEventListener("dragover", onDragOver);
|
||||
window.removeEventListener("dragleave", onDragLeave);
|
||||
window.removeEventListener("drop", onDrop);
|
||||
liveUrls.forEach(releaseObjectUrl);
|
||||
liveUrls.length = 0;
|
||||
});
|
||||
|
||||
return { dragging, busy, notice };
|
||||
}
|
||||
Reference in New Issue
Block a user