This commit is contained in:
yinsx
2026-01-26 09:51:41 +08:00
parent ce796e2fd7
commit d97ea9c791
14 changed files with 626 additions and 150 deletions

View File

@ -45,7 +45,14 @@ const gridRef = ref<HTMLElement | null>(null);
const grid = ref<any | null>(null);
const isDragging = ref(false);
const suppressClick = ref(false);
const clickBlockUntil = ref(0);
const gridOrder = ref<GridOrderEntry[]>([]);
const resizedKeys = ref(new Set<string>());
const pendingResizedKeys = new Set<string>();
const widgetSizeMap = new Map<string, GridItemSize>();
const iconSizeMap = new Map<string, GridItemSize>();
const layoutAnimationMs = 240;
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
const buildDefaultOrder = (): GridOrderEntry[] => [
...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })),
@ -173,6 +180,13 @@ const handleClick = (event: MouseEvent) => {
}
};
const handleClickCapture = (event: MouseEvent) => {
if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
event.preventDefault();
event.stopPropagation();
}
};
const handleContextMenu = (event: MouseEvent) => {
const target = event.target as HTMLElement;
const itemEl = target.closest('.grid-item') as HTMLElement | null;
@ -204,10 +218,40 @@ const persistOrderFromGrid = async () => {
grid.value?.layout();
};
const refreshLayout = async () => {
const refreshLayout = async (instant = false) => {
await nextTick();
grid.value?.refreshItems();
grid.value?.layout();
if (instant) {
grid.value?.layout(true);
} else {
grid.value?.layout();
}
};
const markResized = (type: GridItemType, id: string) => {
pendingResizedKeys.add(`${type}:${id}`);
};
const syncSizeMap = (
items: { id: string; size: GridItemSize }[],
map: Map<string, GridItemSize>
) => {
const changed: string[] = [];
const seen = new Set<string>();
for (const item of items) {
const prev = map.get(item.id);
if (prev && prev !== item.size) {
changed.push(item.id);
}
map.set(item.id, item.size);
seen.add(item.id);
}
for (const id of Array.from(map.keys())) {
if (!seen.has(id)) {
map.delete(id);
}
}
return changed;
};
onMounted(async () => {
@ -226,6 +270,8 @@ onMounted(async () => {
fillGaps: true,
rounding: true,
},
layoutDuration: layoutAnimationMs,
layoutEasing,
});
grid.value.on('dragStart', () => {
@ -235,6 +281,7 @@ onMounted(async () => {
grid.value.on('dragEnd', () => {
isDragging.value = false;
clickBlockUntil.value = Date.now() + 180;
window.setTimeout(() => {
suppressClick.value = false;
}, 0);
@ -244,6 +291,17 @@ onMounted(async () => {
persistOrderFromGrid();
});
grid.value.on('layoutStart', () => {
if (!pendingResizedKeys.size) return;
resizedKeys.value = new Set(pendingResizedKeys);
});
grid.value.on('layoutEnd', () => {
if (!pendingResizedKeys.size) return;
pendingResizedKeys.clear();
resizedKeys.value = new Set<string>();
});
grid.value.layout(true);
});
@ -256,20 +314,35 @@ watch(
);
watch(
() => widgetsStore.widgets.map(widget => widget.size).join('|'),
() => widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
async () => {
const changed = syncSizeMap(
widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
widgetSizeMap
);
for (const id of changed) {
markResized('widget', id);
}
await refreshLayout();
}
);
watch(
() => layoutStore.icons.map(icon => icon.size ?? '1x1').join('|'),
() => layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
async () => {
const changed = syncSizeMap(
layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
iconSizeMap
);
for (const id of changed) {
markResized('icon', id);
}
await refreshLayout();
}
);
onUnmounted(() => {
pendingResizedKeys.clear();
grid.value?.destroy();
grid.value = null;
});
@ -279,6 +352,8 @@ onUnmounted(() => {
<div
ref="gridRef"
class="grid-canvas"
:style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
@click.capture="handleClickCapture"
@click="handleClick"
@contextmenu.prevent="handleContextMenu"
>
@ -286,7 +361,10 @@ onUnmounted(() => {
v-for="item in orderedItems"
:key="`${item.type}-${item.id}`"
class="grid-item"
:class="`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`"
:class="[
`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`,
{ 'is-resized': resizedKeys.has(`${item.type}:${item.id}`) }
]"
:data-id="item.id"
:data-type="item.type"
>
@ -320,6 +398,12 @@ onUnmounted(() => {
height: var(--cell-size);
margin: calc(var(--cell-gap) / 2);
cursor: grab;
will-change: transform;
}
.grid-item.is-resized {
overflow: hidden;
border-radius: $border-radius-small;
}
.grid-item.size-1x2 {
@ -343,10 +427,25 @@ onUnmounted(() => {
.grid-item-content {
width: 100%;
height: 100%;
transform-origin: center;
}
.grid-item.is-resized .grid-item-content {
animation: grid-item-resize-zoom var(--layout-anim-ms) $motion-easing-standard;
}
@keyframes grid-item-resize-zoom {
0% {
transform: scale(0.97);
}
100% {
transform: scale(1);
}
}
.grid-item.muuri-item-dragging {
z-index: $z-index-menu;
cursor: grabbing;
transition: none;
}
</style>