This commit is contained in:
yinsx
2026-01-28 17:51:06 +08:00
parent d97ea9c791
commit a68e380c42
45 changed files with 4980 additions and 151 deletions

View File

@ -4,6 +4,7 @@ import Muuri from 'muuri';
import { useLayoutStore } from '@/store/useLayoutStore';
import { useWidgetsStore } from '@/store/useWidgetsStore';
import { useUIStore } from '@/store/useUIStore';
import { useSettingsStore } from '@/store/useSettingsStore';
import IconCard from '@/components/IconCard/index.vue';
import WidgetCard from '@/components/WidgetCard/index.vue';
@ -15,6 +16,7 @@ interface Icon {
name: string;
url: string;
img?: string;
text?: string;
bgColor?: string;
size?: GridItemSize;
}
@ -41,6 +43,7 @@ const GRID_ORDER_STORAGE_KEY = 'itab_grid_order';
const layoutStore = useLayoutStore();
const widgetsStore = useWidgetsStore();
const uiStore = useUIStore();
const settingsStore = useSettingsStore();
const gridRef = ref<HTMLElement | null>(null);
const grid = ref<any | null>(null);
const isDragging = ref(false);
@ -51,6 +54,9 @@ const resizedKeys = ref(new Set<string>());
const pendingResizedKeys = new Set<string>();
const widgetSizeMap = new Map<string, GridItemSize>();
const iconSizeMap = new Map<string, GridItemSize>();
const enteringKeys = ref(new Set<string>());
const previousIconIds = ref(new Set<string>());
const previousWidgetIds = ref(new Set<string>());
const layoutAnimationMs = 240;
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
@ -59,9 +65,10 @@ const buildDefaultOrder = (): GridOrderEntry[] => [
...layoutStore.icons.map(icon => ({ id: icon.id, type: 'icon' as const })),
];
const persistGridOrder = (order: GridOrderEntry[]) => {
const applyGridOrder = (order: GridOrderEntry[], syncStoreOrder = true) => {
gridOrder.value = order;
localStorage.setItem(GRID_ORDER_STORAGE_KEY, JSON.stringify(order));
if (!syncStoreOrder) return;
const iconOrder = order.filter(item => item.type === 'icon').map(item => item.id);
const widgetOrder = order.filter(item => item.type === 'widget').map(item => item.id);
if (iconOrder.length) {
@ -87,7 +94,7 @@ const loadGridOrder = () => {
if (!gridOrder.value.length) {
gridOrder.value = buildDefaultOrder();
}
persistGridOrder(gridOrder.value);
applyGridOrder(gridOrder.value, true);
};
const ensureOrderConsistency = () => {
@ -124,7 +131,7 @@ const ensureOrderConsistency = () => {
});
if (orderChanged) {
persistGridOrder(nextOrder);
applyGridOrder(nextOrder, false);
} else {
gridOrder.value = nextOrder;
}
@ -176,7 +183,8 @@ const handleClick = (event: MouseEvent) => {
if (!itemEl || itemEl.dataset.type !== 'icon') return;
const link = target.closest('a') as HTMLAnchorElement | null;
if (link && link.href) {
window.open(link.href, '_blank');
const targetMode = settingsStore.openInNewTab ? '_blank' : '_self';
window.open(link.href, targetMode);
}
};
@ -190,7 +198,10 @@ const handleClickCapture = (event: MouseEvent) => {
const handleContextMenu = (event: MouseEvent) => {
const target = event.target as HTMLElement;
const itemEl = target.closest('.grid-item') as HTMLElement | null;
if (!itemEl) return;
if (!itemEl) {
uiStore.openContextMenu(event.clientX, event.clientY, null, 'desktop');
return;
}
const id = itemEl.dataset.id;
const type = itemEl.dataset.type as GridItemType | undefined;
if (id && type) {
@ -211,15 +222,31 @@ const persistOrderFromGrid = async () => {
}
}
if (nextOrder.length) {
persistGridOrder(nextOrder);
applyGridOrder(nextOrder, true);
}
await nextTick();
grid.value?.synchronize();
grid.value?.layout();
};
const syncGridItems = () => {
if (!grid.value || !gridRef.value) return;
const existingItems = grid.value.getItems() ?? [];
const existingElements = new Set(existingItems.map((item: any) => item.getElement()));
const domElements = Array.from(gridRef.value.querySelectorAll('.grid-item'));
const toAdd = domElements.filter(element => !existingElements.has(element));
if (toAdd.length) {
grid.value.add(toAdd, { layout: false });
}
const toRemove = existingItems.filter((item: any) => !gridRef.value?.contains(item.getElement()));
if (toRemove.length) {
grid.value.remove(toRemove, { removeElements: false });
}
};
const refreshLayout = async (instant = false) => {
await nextTick();
syncGridItems();
grid.value?.refreshItems();
if (instant) {
grid.value?.layout(true);
@ -256,6 +283,8 @@ const syncSizeMap = (
onMounted(async () => {
loadGridOrder();
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
await nextTick();
if (!gridRef.value) return;
grid.value = new Muuri(gridRef.value, {
@ -267,7 +296,7 @@ onMounted(async () => {
},
dragSort: true,
layout: {
fillGaps: true,
fillGaps: settingsStore.autoAlign,
rounding: true,
},
layoutDuration: layoutAnimationMs,
@ -297,19 +326,36 @@ onMounted(async () => {
});
grid.value.on('layoutEnd', () => {
if (!pendingResizedKeys.size) return;
pendingResizedKeys.clear();
resizedKeys.value = new Set<string>();
if (pendingResizedKeys.size) {
pendingResizedKeys.clear();
resizedKeys.value = new Set<string>();
}
if (enteringKeys.value.size) {
enteringKeys.value = new Set<string>();
}
});
grid.value.layout(true);
});
watch(
() => [layoutStore.icons.length, widgetsStore.widgets.length],
() => {
() => [layoutStore.icons.map(icon => icon.id), widgetsStore.widgets.map(widget => widget.id)],
([iconIds, widgetIds]) => {
ensureOrderConsistency();
refreshLayout();
const nextIconIds = new Set(iconIds);
const nextWidgetIds = new Set(widgetIds);
const addedIcons = iconIds.filter(id => !previousIconIds.value.has(id));
const addedWidgets = widgetIds.filter(id => !previousWidgetIds.value.has(id));
if (addedIcons.length || addedWidgets.length) {
const nextEntering = new Set(enteringKeys.value);
for (const id of addedIcons) nextEntering.add(`icon:${id}`);
for (const id of addedWidgets) nextEntering.add(`widget:${id}`);
enteringKeys.value = nextEntering;
}
previousIconIds.value = nextIconIds;
previousWidgetIds.value = nextWidgetIds;
const instant = addedIcons.length > 0 || addedWidgets.length > 0;
refreshLayout(instant);
}
);
@ -341,6 +387,28 @@ watch(
}
);
watch(
() => settingsStore.autoAlign,
value => {
if (!grid.value) return;
// Muuri has no public setter for fillGaps; update internal setting and relayout.
grid.value._settings.layout.fillGaps = value;
grid.value.layout(true);
}
);
watch(
() => [
settingsStore.layoutDensity,
settingsStore.iconDensity,
settingsStore.compactSidebar,
],
() => {
if (!grid.value) return;
refreshLayout(true);
}
);
onUnmounted(() => {
pendingResizedKeys.clear();
grid.value?.destroy();
@ -363,7 +431,10 @@ onUnmounted(() => {
class="grid-item"
:class="[
`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`,
{ 'is-resized': resizedKeys.has(`${item.type}:${item.id}`) }
{
'is-resized': resizedKeys.has(`${item.type}:${item.id}`),
'is-entering': enteringKeys.has(`${item.type}:${item.id}`)
}
]"
:data-id="item.id"
:data-type="item.type"
@ -388,7 +459,8 @@ onUnmounted(() => {
-ms-user-select: none; /* IE 10+/Edge 浏览器 */
--cell-size: var(--grid-cell-size);
--cell-gap: var(--grid-gap);
padding: calc(var(--cell-gap) / 2);
--cell-gap-padding: max(var(--cell-gap), 0px);
padding: calc(var(--cell-gap-padding) / 2);
box-sizing: border-box;
}
@ -399,6 +471,7 @@ onUnmounted(() => {
margin: calc(var(--cell-gap) / 2);
cursor: grab;
will-change: transform;
transition: opacity 160ms ease;
}
.grid-item.is-resized {
@ -406,6 +479,10 @@ onUnmounted(() => {
border-radius: $border-radius-small;
}
.grid-item.is-entering {
opacity: 0;
}
.grid-item.size-1x2 {
height: calc(var(--cell-size) * 2 + var(--cell-gap));
}