This commit is contained in:
yinsx
2026-02-05 11:41:47 +08:00
parent bf5a3bc343
commit cbfc2cf974
3 changed files with 89 additions and 60 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/node_modules/ /node_modules/
/public/ /public/
/dist/ /dist/
nul nul/

View File

@ -75,17 +75,21 @@ const initialText = (child: FolderChild) => {
} }
.folder-preview { .folder-preview {
--folder-chip-size: min(20px, calc(var(--icon-size, 72px) * 0.28));
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, var(--folder-chip-size));
grid-template-rows: repeat(2, 1fr); grid-template-rows: repeat(2, var(--folder-chip-size));
gap: 6px; gap: 6px;
padding-top: 6px; padding: 4px;
place-content: center;
box-sizing: border-box; box-sizing: border-box;
} }
.folder-chip { .folder-chip {
width: var(--folder-chip-size);
height: var(--folder-chip-size);
border-radius: 8px; border-radius: 8px;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
color: #222; color: #222;

View File

@ -98,18 +98,21 @@ const mergeOrderSnapshot = ref<GridOrderEntry[] | null>(null);
const mergePendingFolderId = ref<string | null>(null); const mergePendingFolderId = ref<string | null>(null);
const mergePendingInsertIndex = ref<number | null>(null); const mergePendingInsertIndex = ref<number | null>(null);
const OVERLAP_MERGE_THRESHOLD = 0.6; const OVERLAP_MERGE_THRESHOLD = 0.6;
const OVERLAP_HOLD_MS = 100; const OVERLAP_HOLD_MS = 0;
const OVERLAP_DROP_THRESHOLD = 0.35; const OVERLAP_DROP_THRESHOLD = 0.35;
const MERGE_HIT_INSET_RATIO = 0.18; const MERGE_HIT_INSET_RATIO = 0;
const MERGE_HIT_INSET_PX = 6; const MERGE_HIT_INSET_PX = 0;
const POINTER_ROW_TOLERANCE_RATIO = 0.6; const POINTER_ROW_TOLERANCE_RATIO = 0.6;
const POINTER_EDGE_PADDING = 6; const POINTER_EDGE_PADDING = 6;
const lastPointer = ref<{ x: number; y: number } | null>(null); const lastPointer = ref<{ x: number; y: number } | null>(null);
const pointerTargetEl = ref<HTMLElement | null>(null); const pointerTargetEl = ref<HTMLElement | null>(null);
const isPressing = ref(false);
const pressTimer = ref<number | null>(null);
const layoutAnimationMs = 240; const layoutAnimationMs = 240;
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)'; const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
const DELETE_ANIM_MS = 240; const DELETE_ANIM_MS = 240;
const DELETE_ANIM_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)'; const DELETE_ANIM_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
const PRESS_DELAY_MS = 100;
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID; const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId)); const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));
@ -415,6 +418,32 @@ const handleClick = (event: MouseEvent) => {
} }
}; };
const clearPressTimer = () => {
if (pressTimer.value !== null) {
window.clearTimeout(pressTimer.value);
pressTimer.value = null;
}
};
const stopPressing = () => {
clearPressTimer();
isPressing.value = false;
};
const handlePointerDown = (event: PointerEvent) => {
if (event.button !== 0) return;
const target = event.target as HTMLElement | null;
if (!target?.closest('.grid-item')) return;
clearPressTimer();
pressTimer.value = window.setTimeout(() => {
isPressing.value = true;
}, PRESS_DELAY_MS);
};
const handleGlobalPointerUp = () => {
stopPressing();
};
const handleClickCapture = (event: MouseEvent) => { const handleClickCapture = (event: MouseEvent) => {
if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) { if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
event.preventDefault(); event.preventDefault();
@ -798,29 +827,11 @@ const handleMergeDrop = () => {
const dragId = draggingMeta.value.id; const dragId = draggingMeta.value.id;
const dragType = draggingMeta.value.type; const dragType = draggingMeta.value.type;
if (!dragId || !dragType) return; if (!dragId || !dragType) return;
let targetEl = mergeTargetEl.value; let targetEl = mergeTargetEl.value ?? hoverTargetEl.value ?? pointerTargetEl.value;
if (!mergeActive.value || !targetEl) {
targetEl = hoverTargetEl.value;
const dragEl = draggingEl.value;
if (!targetEl && dragEl) {
const stats = findTargetStats(dragEl);
const best = stats?.best ?? null;
if (best && best.ratio >= OVERLAP_MERGE_THRESHOLD) {
targetEl = best.el;
}
}
}
if (!targetEl) { if (!targetEl) {
clearHoverTarget(); clearHoverTarget();
return; return;
} }
if (!mergeActive.value && draggingEl.value) {
const ratio = getOverlapRatio(draggingEl.value, targetEl);
if (ratio < OVERLAP_DROP_THRESHOLD) {
clearHoverTarget();
return;
}
}
const targetId = targetEl.dataset.id; const targetId = targetEl.dataset.id;
const targetType = targetEl.dataset.type as GridItemType | undefined; const targetType = targetEl.dataset.type as GridItemType | undefined;
if (!targetId || !targetType) return; if (!targetId || !targetType) return;
@ -1232,6 +1243,8 @@ onMounted(async () => {
ensureOrderConsistency(); ensureOrderConsistency();
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id)); previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id)); previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
window.addEventListener('pointerup', handleGlobalPointerUp);
window.addEventListener('pointercancel', handleGlobalPointerUp);
await nextTick(); await nextTick();
if (!gridRef.value) return; if (!gridRef.value) return;
grid.value = new Muuri(gridRef.value, { grid.value = new Muuri(gridRef.value, {
@ -1266,6 +1279,7 @@ onMounted(async () => {
grid.value.on('dragStart', (item: any) => { grid.value.on('dragStart', (item: any) => {
isDragging.value = true; isDragging.value = true;
suppressClick.value = true; suppressClick.value = true;
stopPressing();
const el = item.getElement() as HTMLElement; const el = item.getElement() as HTMLElement;
draggingEl.value = el; draggingEl.value = el;
draggingItemRef.value = item; draggingItemRef.value = item;
@ -1305,39 +1319,32 @@ onMounted(async () => {
const now = performance.now(); const now = performance.now();
const pointerTarget = pointerTargetEl.value; const pointerTarget = pointerTargetEl.value;
if (mergeActive.value) {
const locked = mergeTargetEl.value;
if (!locked || !locked.isConnected) {
deactivateMergeTarget(true);
} else if (pointerTarget && pointerTarget !== locked) {
deactivateMergeTarget(true);
mergeCandidateEl.value = pointerTarget;
mergeCandidateAt.value = now;
setHoverTarget(pointerTarget);
} else if (!pointerTarget) {
deactivateMergeTarget(true);
} else {
setHoverTarget(locked);
}
} else {
if (pointerTarget && pointerTarget.isConnected) { if (pointerTarget && pointerTarget.isConnected) {
setHoverTarget(pointerTarget); if (mergeCandidateEl.value !== pointerTarget) {
if (mergeCandidateEl.value === pointerTarget) {
if (now - mergeCandidateAt.value >= OVERLAP_HOLD_MS) {
activateMergeTarget(pointerTarget);
}
} else {
mergeCandidateEl.value = pointerTarget; mergeCandidateEl.value = pointerTarget;
mergeCandidateAt.value = now; mergeCandidateAt.value = now;
if (mergeActive.value && mergeTargetEl.value !== pointerTarget) {
deactivateMergeTarget(false);
}
}
const holdReady = now - mergeCandidateAt.value >= OVERLAP_HOLD_MS;
if (holdReady) {
if (!mergeActive.value || mergeTargetEl.value !== pointerTarget) {
activateMergeTarget(pointerTarget);
} else {
setHoverTarget(pointerTarget);
}
} else {
clearHoverTarget();
} }
} else { } else {
mergeCandidateEl.value = null; mergeCandidateEl.value = null;
mergeCandidateAt.value = 0; mergeCandidateAt.value = 0;
deactivateMergeTarget(true);
clearHoverTarget(); clearHoverTarget();
} }
}
applySortLock(mergeActive.value); applySortLock(!!pointerTarget);
}); });
} }
}); });
@ -1497,6 +1504,9 @@ watch(
onUnmounted(() => { onUnmounted(() => {
pendingResizedKeys.clear(); pendingResizedKeys.clear();
stopPressing();
window.removeEventListener('pointerup', handleGlobalPointerUp);
window.removeEventListener('pointercancel', handleGlobalPointerUp);
grid.value?.destroy(); grid.value?.destroy();
grid.value = null; grid.value = null;
}); });
@ -1505,10 +1515,11 @@ onUnmounted(() => {
<template> <template>
<div <div
ref="gridRef" ref="gridRef"
class="grid-canvas" :class="['grid-canvas', { 'is-dragging': isDragging, 'is-pressing': isPressing }]"
:style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }" :style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
@click.capture="handleClickCapture" @click.capture="handleClickCapture"
@click="handleClick" @click="handleClick"
@pointerdown="handlePointerDown"
@contextmenu.prevent="handleContextMenu" @contextmenu.prevent="handleContextMenu"
> >
<div <div
@ -1576,6 +1587,20 @@ onUnmounted(() => {
transition: opacity 160ms ease; transition: opacity 160ms ease;
} }
.grid-item * {
cursor: inherit;
}
.grid-canvas.is-dragging,
.grid-canvas.is-dragging * {
cursor: grabbing !important;
}
.grid-canvas.is-pressing,
.grid-canvas.is-pressing * {
cursor: grabbing !important;
}
.grid-item.is-resized { .grid-item.is-resized {
overflow: visible; overflow: visible;
} }