1
This commit is contained in:
@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user