This commit is contained in:
yinsx
2026-01-29 11:28:01 +08:00
parent a68e380c42
commit d025691180
15 changed files with 286 additions and 119 deletions

View File

@ -19,6 +19,7 @@ interface Icon {
text?: string;
bgColor?: string;
size?: GridItemSize;
groupId?: string;
}
type WidgetSize = GridItemSize;
@ -27,6 +28,7 @@ interface Widget {
component: string;
size: WidgetSize;
data?: any;
groupId?: string;
}
interface GridOrderEntry {
@ -39,6 +41,11 @@ type OrderedGridItem =
| { type: 'widget'; id: string; widget: Widget };
const GRID_ORDER_STORAGE_KEY = 'itab_grid_order';
const DEFAULT_GROUP_ID = 'home';
const props = defineProps<{
activeGroupId?: string | null;
}>();
const layoutStore = useLayoutStore();
const widgetsStore = useWidgetsStore();
@ -59,6 +66,22 @@ 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)';
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));
const normalizeGridOrder = (order: GridOrderEntry[]) => {
const seen = new Set<string>();
const normalized: GridOrderEntry[] = [];
for (const entry of order) {
if (!entry || typeof entry.id !== 'string') continue;
if (entry.type !== 'icon' && entry.type !== 'widget') continue;
const key = `${entry.type}:${entry.id}`;
if (seen.has(key)) continue;
seen.add(key);
normalized.push(entry);
}
return normalized;
};
const buildDefaultOrder = (): GridOrderEntry[] => [
...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })),
@ -85,7 +108,7 @@ const loadGridOrder = () => {
try {
const parsed = JSON.parse(saved) as GridOrderEntry[];
if (Array.isArray(parsed)) {
gridOrder.value = parsed.filter(item => item?.id && (item.type === 'icon' || item.type === 'widget'));
gridOrder.value = normalizeGridOrder(parsed);
}
} catch {
gridOrder.value = [];
@ -100,26 +123,36 @@ const loadGridOrder = () => {
const ensureOrderConsistency = () => {
const iconIds = new Set(layoutStore.icons.map(icon => icon.id));
const widgetIds = new Set(widgetsStore.widgets.map(widget => widget.id));
const seen = new Set<string>();
const nextOrder: GridOrderEntry[] = [];
const pushEntry = (entry: GridOrderEntry) => {
const key = `${entry.type}:${entry.id}`;
if (seen.has(key)) return;
seen.add(key);
nextOrder.push(entry);
};
for (const entry of gridOrder.value) {
if (entry.type === 'icon' && iconIds.has(entry.id)) {
nextOrder.push(entry);
pushEntry(entry);
}
if (entry.type === 'widget' && widgetIds.has(entry.id)) {
nextOrder.push(entry);
pushEntry(entry);
}
}
for (const widget of widgetsStore.widgets) {
if (!nextOrder.some(item => item.type === 'widget' && item.id === widget.id)) {
nextOrder.push({ id: widget.id, type: 'widget' });
const key = `widget:${widget.id}`;
if (!seen.has(key)) {
pushEntry({ id: widget.id, type: 'widget' });
}
}
for (const icon of layoutStore.icons) {
if (!nextOrder.some(item => item.type === 'icon' && item.id === icon.id)) {
nextOrder.push({ id: icon.id, type: 'icon' });
const key = `icon:${icon.id}`;
if (!seen.has(key)) {
pushEntry({ id: icon.id, type: 'icon' });
}
}
@ -137,6 +170,9 @@ const ensureOrderConsistency = () => {
}
};
const isInActiveGroup = (groupId?: string | null) =>
normalizeGroupId(groupId) === activeGroupId.value;
const orderedItems = computed<OrderedGridItem[]>(() => {
const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon]));
const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget]));
@ -146,13 +182,13 @@ const orderedItems = computed<OrderedGridItem[]>(() => {
for (const entry of gridOrder.value) {
if (entry.type === 'icon') {
const icon = iconsById.get(entry.id);
if (icon) {
if (icon && isInActiveGroup(icon.groupId)) {
items.push({ type: 'icon', id: icon.id, icon });
used.add(`icon:${icon.id}`);
}
} else {
const widget = widgetsById.get(entry.id);
if (widget) {
if (widget && isInActiveGroup(widget.groupId)) {
items.push({ type: 'widget', id: widget.id, widget });
used.add(`widget:${widget.id}`);
}
@ -161,14 +197,14 @@ const orderedItems = computed<OrderedGridItem[]>(() => {
for (const widget of widgetsStore.widgets) {
const key = `widget:${widget.id}`;
if (!used.has(key)) {
if (!used.has(key) && isInActiveGroup(widget.groupId)) {
items.push({ type: 'widget', id: widget.id, widget });
}
}
for (const icon of layoutStore.icons) {
const key = `icon:${icon.id}`;
if (!used.has(key)) {
if (!used.has(key) && isInActiveGroup(icon.groupId)) {
items.push({ type: 'icon', id: icon.id, icon });
}
}
@ -222,7 +258,36 @@ const persistOrderFromGrid = async () => {
}
}
if (nextOrder.length) {
applyGridOrder(nextOrder, true);
if (!gridOrder.value.length) {
applyGridOrder(nextOrder, true);
} else {
const entryKey = (entry: GridOrderEntry) => `${entry.type}:${entry.id}`;
const visibleKeys = new Set(nextOrder.map(entryKey));
const visibleQueue = [...nextOrder];
const merged: GridOrderEntry[] = [];
for (const entry of gridOrder.value) {
if (visibleKeys.has(entryKey(entry))) {
const nextEntry = visibleQueue.shift();
if (nextEntry) {
merged.push(nextEntry);
}
} else {
merged.push(entry);
}
}
const mergedKeys = new Set(merged.map(entryKey));
for (const entry of visibleQueue) {
const key = entryKey(entry);
if (!mergedKeys.has(key)) {
merged.push(entry);
mergedKeys.add(key);
}
}
applyGridOrder(merged, true);
}
}
await nextTick();
grid.value?.synchronize();
@ -247,7 +312,11 @@ const syncGridItems = () => {
const refreshLayout = async (instant = false) => {
await nextTick();
syncGridItems();
grid.value?.synchronize();
grid.value?.refreshItems();
if (grid.value?._settings?.layout) {
grid.value._settings.layout.fillGaps = true;
}
if (instant) {
grid.value?.layout(true);
} else {
@ -283,6 +352,7 @@ const syncSizeMap = (
onMounted(async () => {
loadGridOrder();
ensureOrderConsistency();
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
await nextTick();
@ -296,7 +366,7 @@ onMounted(async () => {
},
dragSort: true,
layout: {
fillGaps: settingsStore.autoAlign,
fillGaps: true,
rounding: true,
},
layoutDuration: layoutAnimationMs,
@ -388,12 +458,10 @@ watch(
);
watch(
() => settingsStore.autoAlign,
value => {
() => props.activeGroupId,
() => {
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);
refreshLayout(true);
}
);
@ -475,8 +543,7 @@ onUnmounted(() => {
}
.grid-item.is-resized {
overflow: hidden;
border-radius: $border-radius-small;
overflow: visible;
}
.grid-item.is-entering {